diff --git a/config/default.js b/config/default.js index 7c4238d3f1..a55575ac30 100644 --- a/config/default.js +++ b/config/default.js @@ -460,5 +460,6 @@ module.exports = { PLATFORMUI_SITE_URL: 'https://platform-ui.topcoder-dev.com', TIMELINE: { REJECTION_EVENT_REASONS: ['Duplicate Event'], + ALLOWED_FILETYPES: ['image/jpeg', 'image/png', 'video/mp4', 'video/x-msvideo', 'video/webm'], }, }; diff --git a/src/shared/components/GUIKit/PhotoVideoPicker/index.jsx b/src/shared/components/GUIKit/PhotoVideoPicker/index.jsx index 114da1ca23..0fb0cb39ff 100644 --- a/src/shared/components/GUIKit/PhotoVideoPicker/index.jsx +++ b/src/shared/components/GUIKit/PhotoVideoPicker/index.jsx @@ -31,6 +31,8 @@ function PhotoVideoPicker({ ]); }} {...options} + accept={['image/jpeg', 'image/png', 'video/mp4', 'video/x-msvideo', 'video/webm']} + maxFiles={3} > {({ getRootProps, getInputProps }) => (
@@ -60,7 +62,7 @@ function PhotoVideoPicker({ ) : null } { - file.length < 3 ? ( + file.length <= 3 ? (
{ + const target = document.getElementById(`${selectedFilterValue.year}-${selectedFilterValue.month}`); + if (target) { + target.scrollIntoView({ behavior: 'smooth' }, true); + } else { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }, [selectedFilterValue]); + const deleteEvent = (id) => { deleteEventById(authToken, id, () => { @@ -96,7 +106,7 @@ function TimelineWallContainer(props) { return (
-
+
top-banner top-banner @@ -153,8 +163,12 @@ function TimelineWallContainer(props) { createNewEvent={(body) => { createNewEvent(authToken, body); }} + onDoneAddEvent={() => { + getTimelineEvents(); + }} getAvatar={getAvatar} userAvatars={userAvatars} + uploading={uploading} /> { @@ -185,6 +199,7 @@ TimelineWallContainer.defaultProps = { auth: null, isAdmin: false, loading: false, + uploading: false, events: [], userAvatars: {}, pendingApprovals: [], @@ -197,6 +212,7 @@ TimelineWallContainer.propTypes = { auth: PT.shape(), isAdmin: PT.bool, loading: PT.bool, + uploading: PT.bool, events: PT.arrayOf(PT.shape()), loadUserDetails: PT.func.isRequired, createNewEvent: PT.func.isRequired, @@ -213,6 +229,7 @@ const mapStateToProps = state => ({ }, isAdmin: state.timelineWall.isAdmin, loading: state.timelineWall.loading, + uploading: state.timelineWall.uploading, events: state.timelineWall.events, userAvatars: state.timelineWall.userAvatars, pendingApprovals: state.timelineWall.pendingApprovals, diff --git a/src/shared/containers/timeline-wall/modal-event-add/index.jsx b/src/shared/containers/timeline-wall/modal-event-add/index.jsx index 07c86d69f2..41eda7aaff 100644 --- a/src/shared/containers/timeline-wall/modal-event-add/index.jsx +++ b/src/shared/containers/timeline-wall/modal-event-add/index.jsx @@ -2,10 +2,11 @@ import React from 'react'; import PT from 'prop-types'; import { Modal } from 'topcoder-react-ui-kit'; import IconCloseGreen from 'assets/images/icon-close-green.svg'; +import LoadingIndicator from 'components/LoadingIndicator'; import style from './styles.scss'; -function ModalEventAdd({ onClose, isAdmin }) { +function ModalEventAdd({ onClose, isAdmin, uploading }) { return ( Confirmation
- - { - isAdmin ? 'Thank you! Your event was submitted for review. You’ll receive an email once the review is completed' - : 'Thank you! Your event was added to the Timeline Wall.' - } - + { + uploading ? ( + + ) : ( + + { + !isAdmin ? 'Thank you! Your event was submitted for review. You’ll receive an email once the review is completed' + : 'Thank you! Your event was added to the Timeline Wall.' + } + + ) + }
@@ -47,6 +55,7 @@ function ModalEventAdd({ onClose, isAdmin }) { ModalEventAdd.defaultProps = { onClose: () => { }, isAdmin: false, + uploading: false, }; /** @@ -55,6 +64,7 @@ ModalEventAdd.defaultProps = { ModalEventAdd.propTypes = { onClose: PT.func, isAdmin: PT.bool, + uploading: PT.bool, }; export default ModalEventAdd; diff --git a/src/shared/containers/timeline-wall/modal-photo-viewer/index.jsx b/src/shared/containers/timeline-wall/modal-photo-viewer/index.jsx index a0f741b1d6..2b8b1fa9cc 100644 --- a/src/shared/containers/timeline-wall/modal-photo-viewer/index.jsx +++ b/src/shared/containers/timeline-wall/modal-photo-viewer/index.jsx @@ -11,9 +11,10 @@ import PhotoVideoItem from 'components/GUIKit/PhotoVideoItem'; import style from './styles.scss'; function ModalPhotoViewer({ onClose, selectedPhoto, photos }) { + const newPhotos = photos.map((photo, index) => ({ ...photo, id: index })); const [localSelectedPhoto, setLocalSelectedPhoto] = useState(selectedPhoto); const selectedPhotoObject = useMemo( - () => _.find(photos, { id: localSelectedPhoto }), [localSelectedPhoto], + () => _.find(newPhotos, { id: localSelectedPhoto }), [localSelectedPhoto], ); return ( diff --git a/src/shared/containers/timeline-wall/styles.scss b/src/shared/containers/timeline-wall/styles.scss index 4e85586f17..cfb126ce8e 100644 --- a/src/shared/containers/timeline-wall/styles.scss +++ b/src/shared/containers/timeline-wall/styles.scss @@ -1,4 +1,4 @@ -@import '~styles/mixins'; +@import "~styles/mixins"; .container { display: flex; @@ -19,6 +19,10 @@ } } +.header-admin { + min-height: 207px; +} + .header-content-1 { font-family: BarlowCondensed, sans-serif; font-weight: 500; diff --git a/src/shared/containers/timeline-wall/timeline-events/add-event/index.jsx b/src/shared/containers/timeline-wall/timeline-events/add-event/index.jsx index 2e7df6d0f2..de5001fc19 100644 --- a/src/shared/containers/timeline-wall/timeline-events/add-event/index.jsx +++ b/src/shared/containers/timeline-wall/timeline-events/add-event/index.jsx @@ -10,6 +10,7 @@ import FormField from 'components/Settings/FormField'; import FormInputDatePicker from 'components/Settings/FormInputDatePicker'; import FormInputTextArea from 'components/Settings/FormInputTextArea'; import PhotoVideoPicker from 'components/GUIKit/PhotoVideoPicker'; +import { config } from 'topcoder-react-utils'; import IconCloseGreen from 'assets/images/icon-close-green.svg'; import IconCloseBlack from 'assets/images/tc-edu/icon-close-big.svg'; import ModalEventAdd from '../../modal-event-add'; @@ -17,7 +18,7 @@ import ModalEventAdd from '../../modal-event-add'; import style from './styles.scss'; function AddEvents({ - className, isAuthenticated, createNewEvent, isAdmin, + className, isAuthenticated, createNewEvent, isAdmin, onDoneAddEvent, uploading, }) { const [formData, setFormData] = useState({ eventName: '', @@ -31,13 +32,7 @@ function AddEvents({ && !!formData.description, [formData]); const submitEvent = () => { - const form = new FormData(); - form.append('title', formData.eventName); - form.append('description', formData.description); - form.append('eventDate', formData.date); - form.append('mediaFiles', formData.files || []); - - createNewEvent(form); + createNewEvent(formData); setFormData({ eventName: '', @@ -157,6 +152,10 @@ function AddEvents({ infoText={'Drag & drop your photo or video here\nYou can upload only up to 3 photos/videos'} infoTextMobile="Drag & drop your photo or video here" btnText="BROWSE" + options={{ + accept: config.TIMELINE.ALLOWED_FILETYPES || [], + maxFiles: 3, + }} />
@@ -190,8 +189,14 @@ function AddEvents({ { showModal ? ( setShowModal(false)} + onClose={() => { + setShowModal(false); + if (isAdmin) { + onDoneAddEvent(); + } + }} isAdmin={isAdmin} + uploading={uploading} /> ) : null } @@ -206,6 +211,7 @@ AddEvents.defaultProps = { className: '', isAuthenticated: false, isAdmin: false, + uploading: false, }; /** @@ -216,6 +222,8 @@ AddEvents.propTypes = { isAuthenticated: PT.bool, createNewEvent: PT.func.isRequired, isAdmin: PT.bool, + onDoneAddEvent: PT.func.isRequired, + uploading: PT.bool, }; export default AddEvents; diff --git a/src/shared/containers/timeline-wall/timeline-events/events/event-item/index.jsx b/src/shared/containers/timeline-wall/timeline-events/events/event-item/index.jsx index 49d1af03d6..cf7acd81a1 100644 --- a/src/shared/containers/timeline-wall/timeline-events/events/event-item/index.jsx +++ b/src/shared/containers/timeline-wall/timeline-events/events/event-item/index.jsx @@ -33,6 +33,7 @@ function EventItem({ 'color-red': eventItem.color === 'red', 'color-purple': eventItem.color === 'purple', })} + id={moment(eventItem.eventDate).format('YYYY-MM')} > {isLeft ? null : (
)} {isLeft ? null : ()} @@ -105,7 +106,7 @@ function EventItem({ onClose={() => { setShowModalPhoto(false); }} - photos={eventItem.media} + photos={eventItem.mediaFiles} /> ) : null} diff --git a/src/shared/containers/timeline-wall/timeline-events/index.jsx b/src/shared/containers/timeline-wall/timeline-events/index.jsx index f78cb5e292..721b555ee4 100644 --- a/src/shared/containers/timeline-wall/timeline-events/index.jsx +++ b/src/shared/containers/timeline-wall/timeline-events/index.jsx @@ -1,7 +1,5 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import PT from 'prop-types'; -import moment from 'moment'; -import _ from 'lodash'; import cn from 'classnames'; import AddEvent from './add-event'; import Events from './events'; @@ -22,41 +20,23 @@ function TimelineEvents({ createNewEvent, getAvatar, userAvatars, + onDoneAddEvent, + uploading, }) { - const [localEvents, setLocalEvents] = useState([]); - - useEffect(() => { - setLocalEvents(_.filter(events, (event) => { - const eventDate = moment(event.eventDate); - if (!selectedFilterValue.year) { - return true; - } - if (eventDate.year() !== selectedFilterValue.year) { - return false; - } - if (selectedFilterValue.month < 0) { - return true; - } - if (eventDate.month() !== selectedFilterValue.month) { - return false; - } - - return true; - })); - }, [events, selectedFilterValue]); - return (
- {localEvents.length ? ( + {events.length ? ( { @@ -67,15 +47,15 @@ function TimelineEvents({ /> ) : null} - {!localEvents.length && !!events.length && selectedFilterValue.month < 0 ? ( + {!events.length && !!events.length && selectedFilterValue.month < 0 ? ( No events have been added for this year. Be the first who adds one. ) : null} - {!localEvents.length && !!events.length && selectedFilterValue.month >= 0 ? ( + {!events.length && !!events.length && selectedFilterValue.month >= 0 ? ( No events have been added for this month. Be the first who adds one. ) : null} - {!localEvents.length && !events.length ? ( + {!events.length && !events.length ? ( No events have been added. Be the first who adds one. ) : null} @@ -111,6 +91,7 @@ TimelineEvents.defaultProps = { isAuthenticated: false, isAdmin: false, userAvatars: {}, + uploading: false, }; /** @@ -128,7 +109,9 @@ TimelineEvents.propTypes = { isAdmin: PT.bool, createNewEvent: PT.func.isRequired, getAvatar: PT.func.isRequired, + onDoneAddEvent: PT.func.isRequired, userAvatars: PT.shape(), + uploading: PT.bool, }; export default TimelineEvents; diff --git a/src/shared/reducers/timelineWall.js b/src/shared/reducers/timelineWall.js index f6a917265a..e1ff42bd84 100644 --- a/src/shared/reducers/timelineWall.js +++ b/src/shared/reducers/timelineWall.js @@ -77,6 +77,18 @@ function onPendingApprovalDone(state, { payload }) { }; } +/** +* Handles onCreateNewEventInit action. +* @param {Object} state Previous state. +* @param {Object} payload The payload. +*/ +function onCreateNewEventInit(state) { + return { + ...state, + uploading: true, + }; +} + /** * Handles onCreateNewEventDone action. * @param {Object} state Previous state. @@ -85,6 +97,7 @@ function onPendingApprovalDone(state, { payload }) { function onCreateNewEventDone(state) { return { ...state, + uploading: false, }; } @@ -134,7 +147,7 @@ function create(state = {}) { [actions.timeline.fetchTimelineEventsDone]: onEventsDone, [actions.timeline.fetchPendingApprovalsInit]: onPendingApprovalInit, [actions.timeline.fetchPendingApprovalsDone]: onPendingApprovalDone, - // [actions.timeline.createNewEventInit]: onCreateNewEventInit, + [actions.timeline.createNewEventInit]: onCreateNewEventInit, [actions.timeline.createNewEventDone]: onCreateNewEventDone, [actions.timeline.fetchUserAvatarInit]: onFetchUserAvatarInit, [actions.timeline.fetchUserAvatarDone]: onFetchUserAvatarDone, diff --git a/src/shared/services/timelineWall.js b/src/shared/services/timelineWall.js index c10060f4c4..215502a28d 100644 --- a/src/shared/services/timelineWall.js +++ b/src/shared/services/timelineWall.js @@ -1,5 +1,4 @@ /* eslint-disable import/prefer-default-export */ -import fetch from 'isomorphic-fetch'; import { config } from 'topcoder-react-utils'; import { logger } from 'topcoder-react-lib'; import _ from 'lodash'; @@ -66,12 +65,22 @@ export const getUserDetails = async (tokenV3) => { * * @returns {Promise} */ -export const createEvent = async (tokenV3, body) => { +export const createEvent = async (tokenV3, formData) => { + const form = new FormData(); + form.append('title', formData.eventName); + form.append('description', formData.description); + form.append('eventDate', formData.date); + if (formData.files) { + form.append('mediaFiles', new File(formData.files || [], formData.eventName)); + } + try { const res = await fetch(`${baseUrl}/timelineEvents`, { method: 'POST', - headers: { Authorization: `Bearer ${tokenV3}` }, - body, + headers: { + Authorization: `Bearer ${tokenV3}`, + }, + body: form, }); return res.json();