diff --git a/zubhub_frontend/zubhub/public/locales/en/translation.json b/zubhub_frontend/zubhub/public/locales/en/translation.json index 98319955..17603951 100644 --- a/zubhub_frontend/zubhub/public/locales/en/translation.json +++ b/zubhub_frontend/zubhub/public/locales/en/translation.json @@ -961,6 +961,7 @@ "createActivity": { "welcomeMsg": { "primary": "Create Activity", + "secondary": "Tell us about your informative activity!", "edit": "Update Activity" }, "inputs": { @@ -1082,9 +1083,10 @@ }, "buttons": { "Next": "Next", - "Prev": "Prev", + "Prev": "Previous", "Submit": "Submit For Review", - "Edit": "Save Changes" + "Edit": "Save Changes", + "create": "Create" } }, @@ -1132,6 +1134,10 @@ "proceed": "Proceed", "success": "activity Created successfully", "forbidden": "You must be staff monitor ao educator to be able to create a new activity " + }, + "modal": { + "primary": "Congratulations your activity has been successfully created!", + "secondary": "Once your activity is published, you can share it with the world!" } }, "edit": { diff --git a/zubhub_frontend/zubhub/src/views/activity_details/ActivityDetails.styles.js b/zubhub_frontend/zubhub/src/views/activity_details/ActivityDetails.styles.js index 8367438c..c2a71c6c 100644 --- a/zubhub_frontend/zubhub/src/views/activity_details/ActivityDetails.styles.js +++ b/zubhub_frontend/zubhub/src/views/activity_details/ActivityDetails.styles.js @@ -84,4 +84,16 @@ export const activityDefailsStyles = theme => ({ gap: 32, marginTop: 20, }, + dialogCloseButton: { + display: 'flex', + justifyContent: 'end', + marginTop: '-1em', + }, + dialogTitle: { + fontWeight: 600, + fontSize: '1em', + }, + dialogText: { + fontSize: '1.1em', + }, }); diff --git a/zubhub_frontend/zubhub/src/views/activity_details/ActivityDetailsV2.jsx b/zubhub_frontend/zubhub/src/views/activity_details/ActivityDetailsV2.jsx index 63603322..c5c6aa7c 100644 --- a/zubhub_frontend/zubhub/src/views/activity_details/ActivityDetailsV2.jsx +++ b/zubhub_frontend/zubhub/src/views/activity_details/ActivityDetailsV2.jsx @@ -23,7 +23,6 @@ import ReactQuill from 'react-quill'; import { useSelector } from 'react-redux'; import { useReactToPrint } from 'react-to-print'; import Html2Pdf from 'html2pdf.js'; - import ZubHubAPI from '../../api'; import { colors } from '../../assets/js/colors'; import { ClapBorderIcon } from '../../assets/js/icons/ClapIcon'; @@ -283,24 +282,21 @@ export default function ActivityDetailsV2(props) { } maxWidth="xs" open={open} onClose={toggleDialog}> -
+
- - Congratulations your Activity has been successfully created! + + {t('activityDetails.activity.create.modal.primary')} - - Share your activity with the world. Post it on the following platforms: + + {t('activityDetails.activity.create.modal.secondary')} -
- -
diff --git a/zubhub_frontend/zubhub/src/views/create_activity/CreateActivity.jsx b/zubhub_frontend/zubhub/src/views/create_activity/CreateActivity.jsx index ec3c2b5d..a2157e73 100644 --- a/zubhub_frontend/zubhub/src/views/create_activity/CreateActivity.jsx +++ b/zubhub_frontend/zubhub/src/views/create_activity/CreateActivity.jsx @@ -1,46 +1,22 @@ import { makeStyles } from '@mui/styles'; -import { - Box, - CircularProgress, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - Grid, - Link, - Typography, - useMediaQuery, -} from '@mui/material'; +import { Box, Dialog, Grid, Typography, useMediaQuery } from '@mui/material'; import React, { useEffect, useRef, useState } from 'react'; -import { CustomButton, Modal, PreviewActivity, TagsInput } from '../../components'; import StepWizard from 'react-step-wizard'; -import { - ArrowBackIosRounded, - ArrowForwardIosRounded, - CloseOutlined, - CloudDoneOutlined, - DoneRounded, - KeyboardBackspaceRounded, -} from '@mui/icons-material'; -import { AiOutlineExclamationCircle } from 'react-icons/ai'; -import { createActivityStyles } from './CreateActivity.styles'; +import { ArrowBackIosRounded, ArrowForwardIosRounded, DoneRounded } from '@mui/icons-material'; +import { useFormik } from 'formik'; +import { useSelector } from 'react-redux'; import clsx from 'clsx'; -import { colors } from '../../assets/js/colors'; +import { CustomButton, PreviewActivity } from '../../components'; +import { createActivityStyles } from './CreateActivity.styles'; import styles from '../../assets/js/styles'; import { useDomElementHeight } from '../../hooks/useDomElementHeight.hook'; -import { toast } from 'react-toastify'; -import { useFormik } from 'formik'; import * as script from './script'; -import CreateActivityStep1 from './create_activity_step1'; import Step1 from './step1/Step1'; import Step2 from './step2/Step2'; -import { useSelector } from 'react-redux'; -const DRAFT_STATUSES = { saved: 'SAVED', saving: 'SAVING', idle: 'IDLE' }; const steps = ['Activity Basics', 'Activity Details']; -let firstRender = true; +const firstRender = true; export default function CreateActivity(props) { const { height } = useDomElementHeight('navbar-root'); @@ -54,13 +30,44 @@ export default function CreateActivity(props) { const [activeStep, setActiveStep] = useState(1); const [state, setState] = useState({ ...JSON.parse(JSON.stringify(script.vars.default_state)) }); - const [draftStatus, setDraftStatus] = useState(DRAFT_STATUSES.idle); const [completedSteps, setcompletedSteps] = useState([]); const [preview, setPreview] = useState(false); const [isLoading, setIsLoading] = useState(false); const isActive = index => index + 1 === activeStep; const isCompleted = index => completedSteps.includes(index + 1); + const { t } = props; + const formikStep1 = useFormik(script.step1Schema); + const formikStep2 = useFormik(script.step2Schema); + + const checkErrors = () => { + if (activeStep === 1) { + return formikStep1.setTouched({ title: true }, true); + } + + if (activeStep === 2) { + return formikStep2.setTouched({ introduction: true }, true); + } + }; + + const go = direction => { + if (direction === 'next') { + if (activeStep !== 3) { + wizardRef.current.nextStep(); + const completedStepsTemp = [...new Set([...completedSteps, activeStep])]; + setcompletedSteps(completedStepsTemp); + if (activeStep !== 2) { + setActiveStep(step => step + 1); + } + } + } + if (direction === 'prev') { + if (activeStep !== 1) { + wizardRef.current.previousStep(); + setActiveStep(step => step - 1); + } + } + }; const handleSetState = obj => { if (obj) { @@ -76,7 +83,7 @@ export default function CreateActivity(props) { setIsLoading(true); script .getActivity({ ...props, auth, formikStep1, formikStep2 }, state) - .then(result => {}) + .then(() => {}) .finally(() => setIsLoading(false)); } } @@ -84,26 +91,9 @@ export default function CreateActivity(props) { const togglePreview = () => setPreview(!preview); - useEffect(() => { - if (isLoading) { - setDraftStatus(DRAFT_STATUSES.saving); - } else { - setDraftStatus(DRAFT_STATUSES.saved); - } - }, [isLoading]); - - const draftContainerText = () => { - if (draftStatus === DRAFT_STATUSES.idle) return 'Draft'; - if (draftStatus === DRAFT_STATUSES.saving) return !isSmallScreen ? 'Saving to draft' : 'Saving...'; - if (draftStatus === DRAFT_STATUSES.saved) return !isSmallScreen ? 'Saved to draft' : ''; - }; - - const formikStep1 = useFormik(script.step1Schema); - const formikStep2 = useFormik(script.step2Schema); - const previous = () => go('prev'); const next = async () => { - let error = await checkErrors(); + const error = await checkErrors(); console.log(error); if (Object.keys(error || {}).length > 0) return; @@ -116,48 +106,19 @@ export default function CreateActivity(props) { step2Values: formikStep2.values, props: { ...props, auth }, state, - handleSetState: handleSetState, + handleSetState, step: activeStep, }, success => { if (success) { - if (activeStep == 1) go('next'); - if (activeStep == 2) props.navigate(`/activities/${props.params.id}?success=true`); + if (activeStep === 1) go('next'); + if (activeStep === 2) props.navigate(`/activities/${props.params.id}?success=true`); setIsLoading(false); } }, ); }; - const checkErrors = () => { - if (activeStep === 1) { - return formikStep1.setTouched({ title: true }, true); - } - - if (activeStep === 2) { - return formikStep2.setTouched({ introduction: true }, true); - } - }; - - const go = direction => { - if (direction === 'next') { - if (activeStep !== 3) { - wizardRef.current.nextStep(); - let completedStepsTemp = [...new Set([...completedSteps, activeStep])]; - setcompletedSteps(completedStepsTemp); - if (activeStep !== 2) { - setActiveStep(step => step + 1); - } - } - } - if (direction === 'prev') { - if (activeStep !== 1) { - wizardRef.current.previousStep(); - setActiveStep(step => step - 1); - } - } - }; - const renderSteps = steps.map((label, index) => ( - {/* Banner */} - - - <> - - Preview - - - {draftStatus === DRAFT_STATUSES.saving ? : null} - {draftStatus === DRAFT_STATUSES.saved ? : null} - - - {draftContainerText()} - - - - - {/* Form */} - Create Activity - Tell us about your informative activity ! + {t('createActivity.welcomeMsg.primary')} + {t('createActivity.welcomeMsg.secondary')} {/* Step Navigation UI */} @@ -226,7 +169,7 @@ export default function CreateActivity(props) { primaryButtonOutlinedStyle startIcon={} > - Previous + {t('createActivity.buttons.Prev')} )} @@ -237,7 +180,7 @@ export default function CreateActivity(props) { primaryButtonStyle endIcon={} > - {activeStep == 2 ? 'Publish' : 'Next'} + {activeStep === 2 ? t('createActivity.buttons.create') : t('createActivity.buttons.Next')} diff --git a/zubhub_frontend/zubhub/src/views/create_activity/script.js b/zubhub_frontend/zubhub/src/views/create_activity/script.js index a80f22bb..544ec171 100644 --- a/zubhub_frontend/zubhub/src/views/create_activity/script.js +++ b/zubhub_frontend/zubhub/src/views/create_activity/script.js @@ -1,796 +1,721 @@ import { nanoid } from 'nanoid'; import * as Yup from 'yup'; import { toast } from 'react-toastify'; +import { uniqueId } from 'lodash'; +import worker from 'workerize-loader!../../assets/js/removeMetaDataWorker'; // eslint-disable-line import/no-webpack-loader-syntax, import/no-unresolved import { s3 as DO, doConfig, Compress, slugify } from '../../assets/js/utils/scripts'; -import worker from 'workerize-loader!../../assets/js/removeMetaDataWorker'; // eslint-disable-line import/no-webpack-loader-syntax import ZubhubAPI from '../../api'; -import { uniqueId } from 'lodash'; + const idPrefix = 'activitystep'; const API = new ZubhubAPI(); export const class_grades = [ - { name: 'Grade 1-3', id: '1-3' }, - { name: 'Grade 4-6', id: '4-6' }, - { name: 'Grade 7-9', id: '7-9' }, - { name: 'Grade 10-12', id: '10-12' }, + { name: 'Grade 1-3', id: '1-3' }, + { name: 'Grade 4-6', id: '4-6' }, + { name: 'Grade 7-9', id: '7-9' }, + { name: 'Grade 10-12', id: '10-12' }, ]; export const vars = { - image_field_touched: false, - video_field_touched: false, - upload_in_progress: false, - timer: { id: null }, - default_state: { - loading: true, - error: null, - desc_input_is_focused: false, - video_upload_dialog_open: false, - select_video_file: false, - materials_used: [], - categories: [], - tag_suggestion: [], - tag_suggestion_open: false, - publish_types: [], - publish_visible_to_suggestion: [], - publish_visible_to_suggestion_open: false, - media_upload: { - upload_dialog: false, - images_to_upload: [], - videos_to_upload: [], - upload_info: {}, - upload_percent: 0, - uploaded_images_url: [], - uploaded_videos_url: [], - }, + image_field_touched: false, + video_field_touched: false, + upload_in_progress: false, + timer: { id: null }, + default_state: { + loading: true, + error: null, + desc_input_is_focused: false, + video_upload_dialog_open: false, + select_video_file: false, + materials_used: [], + categories: [], + tag_suggestion: [], + tag_suggestion_open: false, + publish_types: [], + publish_visible_to_suggestion: [], + publish_visible_to_suggestion_open: false, + media_upload: { + upload_dialog: false, + images_to_upload: [], + videos_to_upload: [], + upload_info: {}, + upload_percent: 0, + uploaded_images_url: [], + uploaded_videos_url: [], }, - quill: { - modules: { - toolbar: [ - ['bold', 'italic', 'underline', 'strike'], - [{ header: [1, 2, 3, 4, 5, 6] }], - [{ list: 'ordered' }, { list: 'bullet' }], - [{ indent: '-1' }, { indent: '+1' }], - ['code-block'], - ], - }, - formats: [ - 'header', - 'bold', - 'italic', - 'underline', - 'strike', - 'blockquote', - 'script', - 'list', - 'bullet', - 'indent', - 'link', - 'image', - 'color', - 'code-block', - ], + }, + quill: { + modules: { + toolbar: [ + ['bold', 'italic', 'underline', 'strike'], + [{ header: [1, 2, 3, 4, 5, 6] }], + [{ list: 'ordered' }, { list: 'bullet' }], + [{ indent: '-1' }, { indent: '+1' }], + ['code-block'], + ], }, + formats: [ + 'header', + 'bold', + 'italic', + 'underline', + 'strike', + 'blockquote', + 'script', + 'list', + 'bullet', + 'indent', + 'link', + 'image', + 'color', + 'code-block', + ], + }, }; - - - - - export const handleMaterialsUsedFieldBlur = props => { - props.setStatus({ ...props.status, materials_used: '' }); - props.setFieldTouched('materials_used', true); + props.setStatus({ ...props.status, materials_used: '' }); + props.setFieldTouched('materials_used', true); }; export const removeMetaData = (images, state, handleSetState) => { - const newWorker = worker(); - newWorker.removeMetaData(images); - newWorker.addEventListener('message', e => { - Compress(e.data, state, handleSetState); - }); + const newWorker = worker(); + newWorker.removeMetaData(images); + newWorker.addEventListener('message', e => { + Compress(e.data, state, handleSetState); + }); }; - -export const initUpload = (state, props, handleSetState) => { - if (!props.auth.token) return props.navigate('/login'); - - if (!(props.values.images.length !== 0 || props.values.video.length !== 0)) { - vars.upload_in_progress = true; - return uploadProject(state, props, handleSetState); - } - - vars.upload_in_progress = true; - state.media_upload.upload_dialog = true; - handleSetState({ - success: false, - default_state: { - loading: true, - }, +export const uploadProject = async (state, props, handleSetState) => { + const materials_used = props.values['materials_used'].filter(value => (value ? true : false)).join(','); + + const tags = props.values['tags'] + ? props.values['tags'].map(tag => (typeof tag === 'string' ? { name: tag } : tag)) + : []; + + const create_or_update = props.params.id ? props.updateProject : props.createProject; + create_or_update({ + ...props.values, + tags, + materials_used, + id: props.params.id, + token: props.auth.token, + activity: props.location.state?.activity_id, + images: state.media_upload.uploaded_images_url || '', + video: state.media_upload.uploaded_videos_url[0] || props.values.video_link, + category: props.values.category.filter(cat => cat.name).map(cat => cat?.name), + t: props.t, + publish: { type: props.step < 3 ? 1 : 4, visible_to: [] }, + }) + .then(id => { + handleSetState({ ...state, default_state: { loading: false }, success: true, id: `${id}` }); + }) + .catch(error => { + console.log(error, 'error'); + handleSetState({ media_upload: { - ...state.media_upload, - upload_dialog: true, - upload_percent: 0, + ...state.media_upload, + upload_dialog: false, + default_state: { loading: false }, }, + }); + const messages = JSON.parse(error.message); + if (typeof messages === 'object') { + const server_errors = {}; + Object.keys(messages).forEach(key => { + if (key === 'non_field_errors') { + server_errors['non_field_errors'] = messages[key][0]; + } else { + server_errors[key] = messages[key][0]; + } + }); + props.setStatus({ ...server_errors }); + } else { + props.setStatus({ + non_field_errors: props.t('createProject.errors.unexpected'), + }); + } + }) + .finally(() => { + vars.upload_in_progress = false; // flag to prevent attempting to upload a project when an upload is already in progress }); +}; - const promises = []; - - // upload images - for (let index = 0; index < props.values.images.filter(img => !img.link).length; index++) { - promises.push(uploadImage(props.values.images[index], state, props, handleSetState)); +export class UploadMedia { + constructor(type, url, formData, state, props, handleSetState, resolve, reject) { + this.xhr = new XMLHttpRequest(); + this.type = type; + this.url = url; + this.formData = formData; + this.state = state; + this.props = props; + this.handleSetState = handleSetState; + this.resolve = resolve; + this.reject = reject; + this.xhr.upload.onload = this.uploadOnLoad; + this.xhr.onreadystatechange = this.onReadyStateChange; + this.xhr.upload.onerror = this.uploadOnerror; + this.xhr.upload.onprogress = this.uploadOnprogress; + } + + uploadOnLoad = () => { + if (this.xhr.status !== 200 && this.xhr.readyState === 4) { + const error = this.props.t('createProject.errors.unexpected'); + this.reject(error); } - - // upload videos - for (let index = 0; index < props.values.video.length; index++) { - promises.push(uploadVideo(props.values.video[index], state, props, handleSetState)); + }; + + onReadyStateChange = () => { + if (this.xhr.status === 200 && this.xhr.readyState === 4 && this.type === 'video') { + const data = JSON.parse(this.xhr.response); + const secure_url = data.secure_url; + this.resolve({ secure_url }); + } else if (this.xhr.status === 200 && this.xhr.readyState === 4 && this.type === 'image') { + const data = JSON.parse(this.xhr.response); + const secure_url = data.Location; + const public_id = data.Key; + + this.resolve({ image_url: secure_url, public_id }); } + }; - // wait for all image and video promises to resolve before continuing - Promise.all(promises) - .then(all => { - const uploaded_images_url = state.media_upload.uploaded_images_url; - const uploaded_videos_url = state.media_upload.uploaded_videos_url; - - all.forEach(each => { - if (each.public_id) { - uploaded_images_url.push(each); - } else if (each.secure_url) { - uploaded_videos_url[0] = each.secure_url; - } - }); - - state = JSON.parse(JSON.stringify(state)); - state.media_upload.uploaded_images_url = uploaded_images_url; - state.media_upload.uploaded_videos_url = uploaded_videos_url; - - uploadProject(state, props, handleSetState); - }) - .catch(error => { - // settimeout is used to delay closing the upload_dialog until - // state have reflected all prior attempts to set state. - // This is to ensure nothing overwrites the dialog closing. - // A better approach would be to refactor the app and use - // redux for most complex state interactions. - setTimeout( - () => - handleSetState({ - media_upload: { ...state.media_upload, upload_dialog: false }, - }), - 2000, - ); - - if (error) toast.warning(error); - }); -}; + uploadOnerror = () => { + const error = this.props.t('createProject.errors.unexpected'); + this.reject(error); + }; -export const uploadProject = async (state, props, handleSetState) => { - const materials_used = props.values['materials_used'] - .filter(value => (value ? true : false)) - .join(','); - - const tags = props.values['tags'] ? props.values['tags'].map(tag => typeof tag === 'string' ? { name: tag } : tag) : []; - - const create_or_update = props.params.id ? props.updateProject : props.createProject; - create_or_update({ - ...props.values, - tags, - materials_used, - id: props.params.id, - token: props.auth.token, - activity: props.location.state?.activity_id, - images: state.media_upload.uploaded_images_url || '', - video: state.media_upload.uploaded_videos_url[0] || props.values.video_link, - category: props.values.category.filter(cat => cat.name).map(cat => cat?.name), - t: props.t, - publish: { type: props.step < 3 ? 1 : 4, visible_to: [] } - }) - .then((id) => { - handleSetState({ ...state, default_state: { loading: false }, success: true, id: `${id}` }) - }) - .catch(error => { - console.log(error, 'error'); - handleSetState({ - media_upload: { - ...state.media_upload, - upload_dialog: false, - default_state: { loading: false } - }, - }); - const messages = JSON.parse(error.message); - if (typeof messages === 'object') { - const server_errors = {}; - Object.keys(messages).forEach(key => { - if (key === 'non_field_errors') { - server_errors['non_field_errors'] = messages[key][0]; - } else { - server_errors[key] = messages[key][0]; - } - }); - props.setStatus({ ...server_errors }); - } else { - props.setStatus({ - non_field_errors: props.t('createProject.errors.unexpected'), - }); - } - }) - .finally(() => { - vars.upload_in_progress = false; // flag to prevent attempting to upload a project when an upload is already in progress - }); -}; + uploadOnprogress = e => { + const progress = Math.round((e.loaded * 100.0) / e.total); + const { media_upload } = this.state; + const upload_info = JSON.parse(JSON.stringify(media_upload.upload_info)); + upload_info[this.formData.get('file').name] = progress; -export const uploadVideo = (video, state, props, handleSetState) => { - if ( - typeof video === 'string' && - video.match( - /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g, - ) - ) { - return new Promise(r => r({ secure_url: video })); - } else { - const args = { - t: props.t, - token: props.auth.token, - }; + let total = 0; + Object.keys(upload_info).forEach(each => { + total += upload_info[each]; + }); - return API.shouldUploadToLocal(args).then(res => { - if (res && res.local === true) { - return uploadVideoToLocal(video, state, props, handleSetState); - } else if (res && res.local === false) { - return uploadVideoToCloudinary(video, state, props, handleSetState); - } - }); + total /= Object.keys(upload_info).length; + + this.handleSetState({ + media_upload: { + ...media_upload, + upload_info, + upload_percent: total, + }, + }); + }; + + upload = () => { + this.xhr.open('POST', this.url); + if (!this.url.startsWith(process.env.REACT_APP_VIDEO_UPLOAD_URL)) { + this.xhr.xsrfCookieName = 'csrftoken'; + this.xhr.xsrfHeaderName = 'X-CSRFToken'; + this.xhr.setRequestHeader('Authorization', `Token ${this.props.auth.token}`); + this.xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); } -}; + this.xhr.send(this.formData); + }; +} -export const uploadVideoToLocal = (video, state, props, handleSetState) => { - let url = - process.env.REACT_APP_NODE_ENV === 'production' - ? process.env.REACT_APP_BACKEND_PRODUCTION_URL + '/api/' - : process.env.REACT_APP_BACKEND_DEVELOPMENT_URL + '/api/'; - url = url + 'upload-file-to-local/'; - - let key = nanoid(); - key = key.slice(0, Math.floor(key.length / 3)); - key = `videos/${slugify(props.auth.username)}-${slugify(video.name)}-${key}`; - - const formData = new FormData(); - formData.append('file', video); - formData.append('key', key); - - return new Promise((resolve, reject) => { - const um = new UploadMedia( - 'video', - url, - formData, - state, - props, - handleSetState, - resolve, - reject, - ); - um.upload(); - }); +export const uploadImageToLocal = (image, state, props, handleSetState) => { + let url = + process.env.REACT_APP_NODE_ENV === 'production' + ? `${process.env.REACT_APP_BACKEND_PRODUCTION_URL}/api/` + : `${process.env.REACT_APP_BACKEND_DEVELOPMENT_URL}/api/`; + url += 'upload-file-to-local/'; + + const formData = new FormData(); + formData.append('file', image); + formData.append('key', `project_images/${nanoid()}`); + return new Promise((resolve, reject) => { + const um = new UploadMedia('image', url, formData, state, props, handleSetState, resolve, reject); + um.upload(); + }); }; +export const uploadImageToDO = (image, state, props, handleSetState) => + new Promise((resolve, reject) => { + const params = { + Bucket: `${doConfig.bucketName}`, + Key: `${doConfig.project_images}/${nanoid()}`, + Body: image, + ContentType: image.type, + ACL: 'public-read', + }; -export const uploadVideoToCloudinary = ( - video, - state, - props, - handleSetState, -) => { - const url = process.env.REACT_APP_VIDEO_UPLOAD_URL; + DO.upload(params, err => { + reject(err.message); + }) + .on('httpUploadProgress', e => { + const progress = Math.round((e.loaded * 100.0) / e.total); + const { media_upload } = state; + const upload_info = JSON.parse(JSON.stringify(media_upload.upload_info)); + upload_info[image.name] = progress; - const upload_preset = - process.env.REACT_APP_NODE_ENV === 'production' - ? process.env.REACT_APP_VIDEO_UPLOAD_PRESET_NAME - : process.env.REACT_APP_DEV_VIDEO_UPLOAD_PRESET_NAME; + let total = 0; + Object.keys(upload_info).forEach(each => { + total += upload_info[each]; + }); - const params = { - upload_preset, - username: props.auth.username, - filename: video.name, - t: props.t, - token: props.auth.token, - }; + total /= Object.keys(upload_info).length; - return props.getSignature(params).then(sig_res => { - if (typeof sig_res === 'object') { - const formData = new FormData(); - formData.append('file', video); - formData.append('public_id', sig_res.public_id); - formData.append('upload_preset', upload_preset); - formData.append('api_key', sig_res.api_key); - formData.append('timestamp', sig_res.timestamp); - formData.append('signature', sig_res.signature); - - return new Promise((resolve, reject) => { - const um = new UploadMedia( - 'video', - url, - formData, - state, - props, - handleSetState, - resolve, - reject, - ); - um.upload(); - }); + handleSetState({ + default_state: { loading: false }, + media_upload: { + ...media_upload, + upload_info, + upload_percent: total, + }, + }); + }) + .send((err, data) => { + if (err) { + if (err.message.startsWith('Unexpected')) { + const error = props.t('createProject.errors.unexpected'); + reject(error); + } else { + reject(err.message); + } } else { - return Promise.reject(''); + const secure_url = data.Location; + const public_id = data.Key; + resolve({ image_url: secure_url, public_id }); } - }); + }); + }); + +export const uploadVideoToLocal = (video, state, props, handleSetState) => { + let url = + process.env.REACT_APP_NODE_ENV === 'production' + ? `${process.env.REACT_APP_BACKEND_PRODUCTION_URL}/api/` + : `${process.env.REACT_APP_BACKEND_DEVELOPMENT_URL}/api/`; + url += 'upload-file-to-local/'; + + let key = nanoid(); + key = key.slice(0, Math.floor(key.length / 3)); + key = `videos/${slugify(props.auth.username)}-${slugify(video.name)}-${key}`; + + const formData = new FormData(); + formData.append('file', video); + formData.append('key', key); + + return new Promise((resolve, reject) => { + const um = new UploadMedia('video', url, formData, state, props, handleSetState, resolve, reject); + um.upload(); + }); }; +export const uploadVideoToCloudinary = (video, state, props, handleSetState) => { + const url = process.env.REACT_APP_VIDEO_UPLOAD_URL; + + const upload_preset = + process.env.REACT_APP_NODE_ENV === 'production' + ? process.env.REACT_APP_VIDEO_UPLOAD_PRESET_NAME + : process.env.REACT_APP_DEV_VIDEO_UPLOAD_PRESET_NAME; + + const params = { + upload_preset, + username: props.auth.username, + filename: video.name, + t: props.t, + token: props.auth.token, + }; + + return props.getSignature(params).then(sig_res => { + if (typeof sig_res === 'object') { + const formData = new FormData(); + formData.append('file', video); + formData.append('public_id', sig_res.public_id); + formData.append('upload_preset', upload_preset); + formData.append('api_key', sig_res.api_key); + formData.append('timestamp', sig_res.timestamp); + formData.append('signature', sig_res.signature); + + return new Promise((resolve, reject) => { + const um = new UploadMedia('video', url, formData, state, props, handleSetState, resolve, reject); + um.upload(); + }); + } else { + return Promise.reject(); + } + }); +}; export const uploadImage = (image, state, props, handleSetState) => { + const args = { + t: props.t, + token: props.auth.token, + }; + + return API.shouldUploadToLocal(args).then(res => { + if (res && res.local === true) { + return uploadImageToLocal(image, state, props, handleSetState); + } else if (res && res.local === false) { + return uploadImageToDO(image, state, props, handleSetState); + } + }); +}; + +export const uploadVideo = (video, state, props, handleSetState) => { + if ( + typeof video === 'string' && + video.match(/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g) // eslint-disable-line no-useless-escape + ) { + return new Promise(r => r({ secure_url: video })); + } else { const args = { - t: props.t, - token: props.auth.token, + t: props.t, + token: props.auth.token, }; return API.shouldUploadToLocal(args).then(res => { - if (res && res.local === true) { - return uploadImageToLocal(image, state, props, handleSetState); - } else if (res && res.local === false) { - return uploadImageToDO(image, state, props, handleSetState); - } + if (res && res.local === true) { + return uploadVideoToLocal(video, state, props, handleSetState); + } else if (res && res.local === false) { + return uploadVideoToCloudinary(video, state, props, handleSetState); + } }); + } }; +export const initUpload = (state, props, handleSetState) => { + if (!props.auth.token) return props.navigate('/login'); -export const uploadImageToLocal = (image, state, props, handleSetState) => { - let url = - process.env.REACT_APP_NODE_ENV === 'production' - ? process.env.REACT_APP_BACKEND_PRODUCTION_URL + '/api/' - : process.env.REACT_APP_BACKEND_DEVELOPMENT_URL + '/api/'; - url = url + 'upload-file-to-local/'; - - const formData = new FormData(); - formData.append('file', image); - formData.append('key', `project_images/${nanoid()}`); - return new Promise((resolve, reject) => { - const um = new UploadMedia( - 'image', - url, - formData, - state, - props, - handleSetState, - resolve, - reject, - ); - um.upload(); - }); -}; + if (!(props.values.images.length !== 0 || props.values.video.length !== 0)) { + vars.upload_in_progress = true; + return uploadProject(state, props, handleSetState); + } + vars.upload_in_progress = true; + state.media_upload.upload_dialog = true; + handleSetState({ + success: false, + default_state: { + loading: true, + }, + media_upload: { + ...state.media_upload, + upload_dialog: true, + upload_percent: 0, + }, + }); + + const promises = []; + + // upload images + for (let index = 0; index < props.values.images.filter(img => !img.link).length; index += 1) { + promises.push(uploadImage(props.values.images[index], state, props, handleSetState)); + } + + // upload videos + for (let index = 0; index < props.values.video.length; index += 1) { + promises.push(uploadVideo(props.values.video[index], state, props, handleSetState)); + } + + // wait for all image and video promises to resolve before continuing + Promise.all(promises) + .then(all => { + const uploaded_images_url = state.media_upload.uploaded_images_url; + const uploaded_videos_url = state.media_upload.uploaded_videos_url; + + all.forEach(each => { + if (each.public_id) { + uploaded_images_url.push(each); + } else if (each.secure_url) { + uploaded_videos_url[0] = each.secure_url; + } + }); -export const uploadImageToDO = (image, state, props, handleSetState) => { - return new Promise((resolve, reject) => { - const params = { - Bucket: `${doConfig.bucketName}`, - Key: `${doConfig.project_images}/${nanoid()}`, - Body: image, - ContentType: image.type, - ACL: 'public-read', - }; + state = JSON.parse(JSON.stringify(state)); + state.media_upload.uploaded_images_url = uploaded_images_url; + state.media_upload.uploaded_videos_url = uploaded_videos_url; - DO.upload(params, err => { - reject(err.message); - }) - .on('httpUploadProgress', e => { - const progress = Math.round((e.loaded * 100.0) / e.total); - const { media_upload } = state; - const upload_info = JSON.parse( - JSON.stringify(media_upload.upload_info), - ); - upload_info[image.name] = progress; - - let total = 0; - Object.keys(upload_info).forEach(each => { - total = total + upload_info[each]; - }); - - total = total / Object.keys(upload_info).length; - - handleSetState({ - default_state: { loading: false }, - media_upload: { - ...media_upload, - upload_info, - upload_percent: total, - }, - }); - }) - .send((err, data) => { - if (err) { - if (err.message.startsWith('Unexpected')) { - const error = props.t('createProject.errors.unexpected'); - reject(error); - } else { - reject(err.message); - } - } else { - const secure_url = data.Location; - const public_id = data.Key; - resolve({ image_url: secure_url, public_id }); - } - }); + uploadProject(state, props, handleSetState); + }) + .catch(error => { + // settimeout is used to delay closing the upload_dialog until + // state have reflected all prior attempts to set state. + // This is to ensure nothing overwrites the dialog closing. + // A better approach would be to refactor the app and use + // redux for most complex state interactions. + setTimeout( + () => + handleSetState({ + media_upload: { ...state.media_upload, upload_dialog: false }, + }), + 2000, + ); + + if (error) toast.warning(error); }); }; - -export const getActivity = (props, state) => { - - return API.getActivity({ - id: props.params.id, - token: props.auth.token, - }) - .then(obj => { - if (obj) { - const { formikStep1, formikStep2 } = props - const step1Data = {} - const step2Data = {} - - - if (obj.title) { - step1Data.title = obj.title - } - - if (obj.category) { - step1Data.category = obj.category.map(cat => ({ name: cat, id: cat })) - } - - if (obj.class_grade) { - step1Data.class_grade = class_grades.find(item => item.name === obj.class_grade) - } - - if (obj.introduction) { - step2Data.introduction = obj.introduction - } - - if (obj.images) { - step2Data.images = obj.images?.map(img => ({ ...img.image, name: img.image.file_url, link: true })) - } - - if (obj.materials_used) { - step2Data.materials_used = obj.materials_used - } - - if (obj.materials_used_image) { - step2Data.materials_used_image = [{ ...obj.materials_used_image, link: true, name: obj.materials_used_image.file_url }] - } - - if (obj.making_steps) { - const steps = obj.making_steps.map(step => { - step.images = step.image.map(img => ({ ...img, name: img.file_url, link: true })) - delete step.image - return { ...step, id: uniqueId(idPrefix) } - }) - step2Data.making_steps = steps; - } - - formikStep1.setValues(step1Data, true) - formikStep2.setValues(step2Data, true) - } - return obj +export const getActivity = props => + API.getActivity({ + id: props.params.id, + token: props.auth.token, + }).then(obj => { + if (obj) { + const { formikStep1, formikStep2 } = props; + const step1Data = {}; + const step2Data = {}; + + if (obj.title) { + step1Data.title = obj.title; + } + + if (obj.category) { + step1Data.category = obj.category.map(cat => ({ name: cat, id: cat })); + } + + if (obj.class_grade) { + step1Data.class_grade = class_grades.find(item => item.name === obj.class_grade); + } + + if (obj.introduction) { + step2Data.introduction = obj.introduction; + } + + if (obj.images) { + step2Data.images = obj.images?.map(img => ({ ...img.image, name: img.image.file_url, link: true })); + } + + if (obj.materials_used) { + step2Data.materials_used = obj.materials_used; + } + + if (obj.materials_used_image) { + step2Data.materials_used_image = [ + { ...obj.materials_used_image, link: true, name: obj.materials_used_image.file_url }, + ]; + } + + if (obj.making_steps) { + const steps = obj.making_steps.map(step => { + step.images = step.image.map(img => ({ ...img, name: img.file_url, link: true })); + delete step.image; + return { ...step, id: uniqueId(idPrefix) }; }); -}; + step2Data.making_steps = steps; + } + formikStep1.setValues(step1Data, true); + formikStep2.setValues(step2Data, true); + } + return obj; + }); export const step1Validation = Yup.object().shape({ - title: Yup.string().max(100, 'max').required('required'), - class_grade: Yup.object().shape({ name: Yup.string() }).nullable(), - category: Yup.array() - .of( - Yup.object().shape({ - name: Yup.string().required('Name is required') - }) - ), + title: Yup.string().max(100, 'max').required('required'), + class_grade: Yup.object().shape({ name: Yup.string() }).nullable(), + category: Yup.array().of( + Yup.object().shape({ + name: Yup.string().required('Name is required'), + }), + ), }); export const step1Schema = { - initialValues: { - title: undefined, - class_grade: null, - category: [], - }, - validationSchema: step1Validation, -} + initialValues: { + title: undefined, + class_grade: null, + category: [], + }, + validationSchema: step1Validation, +}; export const step2Validation = Yup.object().shape({ - introduction: Yup.string().max(1000, 'max').required('required'), - materials_used: Yup.string(), - images: Yup.array(), - making_steps: Yup.array().of(Yup.object().shape({ - title: Yup.string(), - description: Yup.string().required(), - step_order: Yup.number(), - images: Yup.array(), - materials_used_image: Yup.array() - })) + introduction: Yup.string().max(1000, 'max').required('required'), + materials_used: Yup.string(), + images: Yup.array(), + making_steps: Yup.array().of( + Yup.object().shape({ + title: Yup.string(), + description: Yup.string().required(), + step_order: Yup.number(), + images: Yup.array(), + materials_used_image: Yup.array(), + }), + ), }); export const step2Schema = { - initialValues: { - introduction: undefined, - images: [], - materials_used: '
', - making_steps: [], - materials_used_image: [] - }, - validationSchema: step2Validation, -} - -export class UploadMedia { - constructor( - type, - url, - formData, - state, - props, - handleSetState, - resolve, - reject, - ) { - this.xhr = new XMLHttpRequest(); - this.type = type; - this.url = url; - this.formData = formData; - this.state = state; - this.props = props; - this.handleSetState = handleSetState; - this.resolve = resolve; - this.reject = reject; - this.xhr.upload.onload = this.uploadOnLoad; - this.xhr.onreadystatechange = this.onReadyStateChange; - this.xhr.upload.onerror = this.uploadOnerror; - this.xhr.upload.onprogress = this.uploadOnprogress; - } - - uploadOnLoad = () => { - if (this.xhr.status !== 200 && this.xhr.readyState === 4) { - const error = this.props.t('createProject.errors.unexpected'); - this.reject(error); - } - }; - - onReadyStateChange = () => { - if ( - this.xhr.status === 200 && - this.xhr.readyState === 4 && - this.type === 'video' - ) { - const data = JSON.parse(this.xhr.response); - const secure_url = data.secure_url; - this.resolve({ secure_url }); - } else if ( - this.xhr.status === 200 && - this.xhr.readyState === 4 && - this.type === 'image' - ) { - const data = JSON.parse(this.xhr.response); - const secure_url = data.Location; - const public_id = data.Key; - - this.resolve({ image_url: secure_url, public_id }); - } - }; - - uploadOnerror = _ => { - const error = this.props.t('createProject.errors.unexpected'); - - this.reject(error); - }; - - uploadOnprogress = e => { - const progress = Math.round((e.loaded * 100.0) / e.total); - const { media_upload } = this.state; - - const upload_info = JSON.parse(JSON.stringify(media_upload.upload_info)); - upload_info[this.formData.get('file').name] = progress; - - let total = 0; - Object.keys(upload_info).forEach(each => { - total = total + upload_info[each]; - }); - - total = total / Object.keys(upload_info).length; - - this.handleSetState({ - media_upload: { - ...media_upload, - upload_info, - upload_percent: total, - }, - }); - }; - - upload = () => { - this.xhr.open('POST', this.url); - if (!this.url.startsWith(process.env.REACT_APP_VIDEO_UPLOAD_URL)) { - this.xhr.xsrfCookieName = 'csrftoken'; - this.xhr.xsrfHeaderName = 'X-CSRFToken'; - this.xhr.setRequestHeader( - 'Authorization', - `Token ${this.props.auth.token}`, - ); - this.xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - } - - this.xhr.send(this.formData); - }; -} - - -export const submitForm = async ({ step1Values, step2Values, props, state, handleSetState, step }, callback) => { - - let fileUploadResponse; - const uploadedImages = { images: [], materials_used_image: [], making_steps: {} }; - const imagesToUpload = { images: [], materials_used_image: [], making_steps: {} }; - - if (step === 2) { - // Add all images present in th form in the imagesToUpload object for upload. - - if (step2Values) { - step2Values.images.forEach(img => { - if (img.link) uploadedImages.images = [...uploadedImages.images, img] - else imagesToUpload.images = [...imagesToUpload.images, img] - }) + initialValues: { + introduction: undefined, + images: [], + materials_used: '
', + making_steps: [], + materials_used_image: [], + }, + validationSchema: step2Validation, +}; - step2Values.materials_used_image?.forEach(img => { - if (img.link) uploadedImages.materials_used_image = [...uploadedImages.materials_used_image, img] - else imagesToUpload.materials_used_image = [...imagesToUpload.materials_used_image, img] - }) +const formFilesUpload = (files, props, state, handleSetState) => { + // The files parametter is in the format + // {[key]:value} where : + // - key represents the field + // - value represents an array of files belonging to the field. + // - Value can also be an object of keys with their arrays values. + // - returns a Promise containing {name:string, files:[]}[] or error - step2Values.making_steps?.forEach((step, index) => { - uploadedImages.making_steps[`${index}`] = step.images.filter(img => img.link) - imagesToUpload.making_steps[`${index}`] = step.images.filter(img => !img.link); - }) - } + const promises = new Map(); - // Upload all files present in the form and get their remote data. - fileUploadResponse = await formFilesUpload(imagesToUpload, props, state, handleSetState); + Object.keys(files).forEach(field => { + const list = files[field]; + const isArrayFilled = Array.isArray(list) && list.length > 0; + const isObjectFilled = !isArrayFilled && Object.keys(list).length > 0; - if (fileUploadResponse.error) { - alert('Failed to upload Files; Please try again'); - return; - } - } + if (isArrayFilled) { + const fieldFilesUploadPromises = []; - const formDataStep1 = { - category: step1Values.category.map(item => item.name), - class_grade: step1Values.class_grade.name, - title: step1Values.title + list.forEach(file => fieldFilesUploadPromises.push(uploadImage(file, state, props, handleSetState))); + promises.set(field, fieldFilesUploadPromises); } - // Add - const formData = { - ...formDataStep1, - ...step === 2 ? step2Values : {}, - publish: step === 2 ? true : false - }; - - if (step === 2) { - let makinStepsImages = fileUploadResponse.filter(item => item.name.startsWith('making_steps')) - let images = fileUploadResponse.filter(item => item.name === 'images') - let materials_used_image = fileUploadResponse.find(item => item.name === 'materials_used_image') - - images = images[0]?.files.map(item => ({ image: replaceImageUrlWithFileUrl(item) })) - formData['images'] = [...(images || []), ...uploadedImages.images.map(item => ({ image: replaceImageUrlWithFileUrl(item) }))] - - if (materials_used_image) { - formData['materials_used_image'] = { - file_url: materials_used_image.files[0].image_url, - public_id: materials_used_image.files[0].public_id - } - } - - if (!materials_used_image && uploadedImages.materials_used_image[0]) { - materials_used_image = uploadedImages.materials_used_image[0] - formData['materials_used_image'] = { - file_url: materials_used_image.file_url, - public_id: materials_used_image.public_id - } - } - - - // replace non uploaded images in formData fields with uploaded images - - // Assign making steps uploaded and formated images to their associated steps - if (makinStepsImages.length > 0) { - const makingStepsWithUploadedImages = formData.making_steps.map((stepData, index) => { - const uploaded = uploadedImages.making_steps[index] - const data = { - ...stepData, - image: [...makinStepsImages[index].files.map(item => replaceImageUrlWithFileUrl(item)), ... (uploaded || [])], - step_order: index + 1 - }; - delete data.id; - delete data.images - return data; - }) + if (isObjectFilled) { + Object.keys(list).forEach((key, index) => { + const fieldFilesUploadPromises = []; - formData.making_steps = makingStepsWithUploadedImages - } + list[key].forEach(file => fieldFilesUploadPromises.push(uploadImage(file, state, props, handleSetState))); + promises.set(`${field}[${index}]`, fieldFilesUploadPromises); + }); } + }); - const activityId = props.params?.id - let createOrUpdateResponse; + const promisesArray = Array.from(promises.entries()); - if (props.params.id) { - // API call to the update activity - createOrUpdateResponse = await API.updateActivity(props.auth.token, activityId, formData) - } + return Promise.all( + promisesArray.map(([name, promise]) => Promise.all(promise).then(files => ({ name, files }))), + ).catch(err => ({ error: err })); +}; - else { - // API call to the create activity - createOrUpdateResponse = await API.createActivity(props.auth?.token, formData); +export const submitForm = async ({ step1Values, step2Values, props, state, handleSetState, step }, callback) => { + let fileUploadResponse; + const uploadedImages = { images: [], materials_used_image: [], making_steps: {} }; + const imagesToUpload = { images: [], materials_used_image: [], making_steps: {} }; + + if (step === 2) { + // Add all images present in th form in the imagesToUpload object for upload. + + if (step2Values) { + step2Values.images.forEach(img => { + if (img.link) uploadedImages.images = [...uploadedImages.images, img]; + else imagesToUpload.images = [...imagesToUpload.images, img]; + }); + + step2Values.materials_used_image?.forEach(img => { + if (img.link) uploadedImages.materials_used_image = [...uploadedImages.materials_used_image, img]; + else imagesToUpload.materials_used_image = [...imagesToUpload.materials_used_image, img]; + }); + + step2Values.making_steps?.forEach((step, index) => { + uploadedImages.making_steps[`${index}`] = step.images.filter(img => img.link); + imagesToUpload.making_steps[`${index}`] = step.images.filter(img => !img.link); + }); } - const data = await createOrUpdateResponse.json(); + // Upload all files present in the form and get their remote data. + fileUploadResponse = await formFilesUpload(imagesToUpload, props, state, handleSetState); - if (data) { - if (!activityId) { - props.navigate(`/activities/${data.id}/edit`, { replace: true }); - } - callback(true) + if (fileUploadResponse.error) { + alert('Failed to upload Files; Please try again'); + return; } - else { - callback(false) - } -} + } -const replaceImageUrlWithFileUrl = (obj) => { + const replaceImageUrlWithFileUrl = obj => { if (!('file_url' in obj)) { - obj = { ...obj, file_url: obj.image_url } + obj = { ...obj, file_url: obj.image_url }; } - delete obj.image_url - - return obj -} - -const formFilesUpload = (files, props, state, handleSetState) => { - // The files parametter is in the format - // {[key]:value} where : - // - key represents the field - // - value represents an array of files belonging to the field. - // - Value can also be an object of keys with their arrays values. - // - returns a Promise containing {name:string, files:[]}[] or error + delete obj.image_url; + + return obj; + }; + + const formDataStep1 = { + category: step1Values.category.map(item => item.name), + class_grade: step1Values.class_grade.name, + title: step1Values.title, + }; + + // Add + const formData = { + ...formDataStep1, + ...(step === 2 ? step2Values : {}), + publish: false, + }; + + if (step === 2) { + const makinStepsImages = fileUploadResponse.filter(item => item.name.startsWith('making_steps')); + let images = fileUploadResponse.filter(item => item.name === 'images'); + let materials_used_image = fileUploadResponse.find(item => item.name === 'materials_used_image'); + + images = images[0]?.files.map(item => ({ image: replaceImageUrlWithFileUrl(item) })); + formData['images'] = [ + ...(images || []), + ...uploadedImages.images.map(item => ({ image: replaceImageUrlWithFileUrl(item) })), + ]; + + if (materials_used_image) { + formData['materials_used_image'] = { + file_url: materials_used_image.files[0].image_url, + public_id: materials_used_image.files[0].public_id, + }; + } - let promises = new Map(); + if (!materials_used_image && uploadedImages.materials_used_image[0]) { + materials_used_image = uploadedImages.materials_used_image[0]; + formData['materials_used_image'] = { + file_url: materials_used_image.file_url, + public_id: materials_used_image.public_id, + }; + } - Object.keys(files).forEach(field => { - const list = files[field]; - const isArrayFilled = Array.isArray(list) && list.length > 0; - const isObjectFilled = !isArrayFilled && Object.keys(list).length > 0; + // replace non uploaded images in formData fields with uploaded images - if (isArrayFilled) { - const fieldFilesUploadPromises = []; + // Assign making steps uploaded and formated images to their associated steps + if (makinStepsImages.length > 0) { + const makingStepsWithUploadedImages = formData.making_steps.map((stepData, index) => { + const uploaded = uploadedImages.making_steps[index]; + const data = { + ...stepData, + image: [...makinStepsImages[index].files.map(item => replaceImageUrlWithFileUrl(item)), ...(uploaded || [])], + step_order: index + 1, + }; + delete data.id; + delete data.images; + return data; + }); - list.forEach(file => fieldFilesUploadPromises.push(uploadImage(file, state, props, handleSetState))) - promises.set(field, fieldFilesUploadPromises); - } + formData.making_steps = makingStepsWithUploadedImages; + } + } - if (isObjectFilled) { - Object.keys(list).forEach((key, index) => { - const fieldFilesUploadPromises = []; + const activityId = props.params?.id; + let createOrUpdateResponse; - list[key].forEach(file => fieldFilesUploadPromises.push(uploadImage(file, state, props, handleSetState))); - promises.set(`${field}[${index}]`, fieldFilesUploadPromises); - }) - } - }); + if (props.params.id) { + // API call to the update activity + createOrUpdateResponse = await API.updateActivity(props.auth.token, activityId, formData); + } else { + // API call to the create activity + createOrUpdateResponse = await API.createActivity(props.auth?.token, formData); + } - const promisesArray = Array.from(promises.entries()); + const data = await createOrUpdateResponse.json(); - return Promise.all(promisesArray.map(([name, promise]) => - Promise.all(promise).then(files => ({ name, files })) - )).catch(err => ({ error: err })); -} \ No newline at end of file + if (data) { + if (!activityId) { + props.navigate(`/activities/${data.id}/edit`, { replace: true }); + } + callback(true); + } else { + callback(false); + } +};