From f7c9cfbbaeb4d14787e81c4f7d1a780215954935 Mon Sep 17 00:00:00 2001 From: Juan Carlos Farah Date: Tue, 30 Apr 2019 20:51:34 +0200 Subject: [PATCH] feat: allow users to visit an online space closes #39 --- package.json | 4 +- public/electron.js | 101 +++++++------ src/actions/common.js | 24 ++++ src/actions/space/index.js | 174 +++++++++++++++-------- src/components/VisitSpace.js | 96 ++++++++++++- src/components/common/MediaCard.js | 28 +--- src/components/phase/PhaseImage.js | 26 +++- src/components/phase/PhaseItems.js | 85 +++++------ src/components/space/SavedSpaces.js | 59 ++++---- src/components/space/SpaceDescription.js | 14 +- src/components/space/SpaceNotFound.js | 19 +++ src/components/space/SpaceScreen.js | 21 ++- src/config/constants.js | 11 +- src/config/endpoints.js | 5 + src/config/messages.js | 21 ++- src/config/rest.js | 21 +++ src/reducers/SpaceReducer.js | 4 +- src/types/space/index.js | 2 +- yarn.lock | 5 + 19 files changed, 472 insertions(+), 248 deletions(-) create mode 100644 src/actions/common.js create mode 100644 src/components/space/SpaceNotFound.js create mode 100644 src/config/endpoints.js create mode 100644 src/config/rest.js diff --git a/package.json b/package.json index 53b85ea0..fc1dfc32 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "postinstall": "electron-builder install-app-deps", "predist": "yarn build", "lint": "eslint .", - "prettier": "prettier --check '{src,public}/**/*.js'", + "prettier:check": "prettier --check '{src,public}/**/*.js'", + "prettier:write": "prettier --write '{src,public}/**/*.js'", "test": "react-scripts test", "dist": "env-cmd ./.env electron-builder", "prerelease": "yarn build", @@ -64,6 +65,7 @@ "ncp": "2.0.0", "prop-types": "15.7.2", "react": "16.8.6", + "react-detect-offline": "2.3.0", "react-dev-utils": "9.0.0", "react-dom": "16.8.6", "react-loading": "2.0.3", diff --git a/public/electron.js b/public/electron.js index 204dcd9a..7383cdaa 100644 --- a/public/electron.js +++ b/public/electron.js @@ -19,6 +19,10 @@ const { ncp } = require('ncp'); const log = require('electron-log'); const { autoUpdater } = require('electron-updater'); const Sentry = require('@sentry/electron'); +const util = require('util'); + +// convert fs.readFile into promise version of same +const readFile = util.promisify(fs.readFile); const { DELETE_SPACE_CHANNEL, @@ -208,44 +212,50 @@ app.on('ready', () => { createWindow(); generateMenu(); - ipcMain.on(GET_SPACE_CHANNEL, async (event, { id, spaces }) => { + ipcMain.on(GET_SPACE_CHANNEL, async (event, { id }) => { try { - const space = spaces.find(el => Number(el.id) === Number(id)); - const { phases } = space; - // eslint-disable-next-line no-restricted-syntax - for (const phase of phases) { - const { items = [] } = phase; - for (let i = 0; i < items.length; i += 1) { - const { resource } = items[i]; - if (resource) { - const { uri, hash, type } = resource; - const fileName = `${hash}.${type}`; - const filePath = `${savedSpacesPath}/${fileName}`; - // eslint-disable-next-line no-await-in-loop - const fileAvailable = await checkFileAvailable(filePath); - if (fileAvailable) { - phase.items[i].asset = filePath; - } else { - // eslint-disable-next-line no-await-in-loop - const isConnected = await isOnline(); - if (isConnected) { - // eslint-disable-next-line no-await-in-loop - await download(mainWindow, uri, { - directory: savedSpacesPath, - filename: fileName, - }).then(dl => { - phase.items[i].asset = dl.getSavePath(); - }); - } else { - mainWindow.webContents.send(GET_SPACE_CHANNEL, ERROR_GENERAL); - } - } - } - } - } + const spacesPath = `${savedSpacesPath}/${spacesFileName}`; + const data = await readFile(spacesPath, 'utf8'); + const spaces = JSON.parse(data); + // id is a string + const space = spaces.find(el => el.id === id); + + // const { phases } = space; + // // eslint-disable-next-line no-restricted-syntax + // for (const phase of phases) { + // const { items = [] } = phase; + // for (let i = 0; i < items.length; i += 1) { + // const { resource } = items[i]; + // if (resource) { + // const { uri, hash, type } = resource; + // const fileName = `${hash}.${type}`; + // const filePath = `${savedSpacesPath}/${fileName}`; + // // eslint-disable-next-line no-await-in-loop + // const fileAvailable = await checkFileAvailable(filePath); + // if (fileAvailable) { + // phase.items[i].asset = filePath; + // } else { + // // eslint-disable-next-line no-await-in-loop + // const isConnected = await isOnline(); + // if (isConnected) { + // // eslint-disable-next-line no-await-in-loop + // await download(mainWindow, uri, { + // directory: savedSpacesPath, + // filename: fileName, + // }).then(dl => { + // phase.items[i].asset = dl.getSavePath(); + // }); + // } else { + // mainWindow.webContents.send(GET_SPACE_CHANNEL, ERROR_GENERAL); + // } + // } + // } + // } + // } mainWindow.webContents.send(GET_SPACE_CHANNEL, space); } catch (err) { log.error(err); + mainWindow.webContents.send(GET_SPACE_CHANNEL, null); } }); ipcMain.on(GET_SPACES_CHANNEL, async () => { @@ -375,15 +385,16 @@ app.on('ready', () => { }); ipcMain.on(LOAD_SPACE_CHANNEL, async (event, { fileLocation }) => { try { - const extractPath = `${savedSpacesPath}/temp/`; - extract(fileLocation, { dir: extractPath }, async err => { - if (err) { - log.error(err); + const extractPath = `${savedSpacesPath}/tmp`; + extract(fileLocation, { dir: extractPath }, async extractError => { + if (extractError) { + log.error(extractError); } else { let space = {}; const spacePath = `${extractPath}/space.json`; - fs.readFile(spacePath, 'utf8', async (error, data) => { - if (error) { + fs.readFile(spacePath, 'utf8', async (readFileError, data) => { + if (readFileError) { + log.error(readFileError); mainWindow.webContents.send( LOADED_SPACE_CHANNEL, ERROR_ZIP_CORRUPTED @@ -423,10 +434,9 @@ app.on('ready', () => { ERROR_JSON_CORRUPTED ); } - const spaceId = Number(space.id); - const available = spaces.find( - ({ id }) => Number(id) === spaceId - ); + // space id is a string + const spaceId = space.id; + const available = spaces.find(({ id }) => id === spaceId); if (!available) { spaces.push(space); const spacesString = JSON.stringify(spaces); @@ -470,7 +480,8 @@ app.on('ready', () => { EXPORT_SPACE_CHANNEL, async (event, { archivePath, id, spaces }) => { try { - const space = spaces.find(el => Number(el.id) === Number(id)); + // space ids are strings + const space = spaces.find(el => el.id === id); const { phases, image: imageUrl } = space; const spacesString = JSON.stringify(space); const ssPath = `${savedSpacesPath}/space.json`; diff --git a/src/actions/common.js b/src/actions/common.js new file mode 100644 index 00000000..a15febb1 --- /dev/null +++ b/src/actions/common.js @@ -0,0 +1,24 @@ +import { UNEXPECTED_ERROR_MESSAGE } from '../config/messages'; + +const createFlag = type => payload => dispatch => + dispatch({ + type, + payload, + }); + +const isErrorResponse = async response => { + const LOWEST_HTTP_ERROR_CODE = 400; + const HIGHEST_HTTP_ERROR_CODE = 599; + + if ( + response.status >= LOWEST_HTTP_ERROR_CODE && + response.status <= HIGHEST_HTTP_ERROR_CODE + ) { + // assumes response has a message property + const { message = UNEXPECTED_ERROR_MESSAGE } = await response.json(); + + throw Error(message); + } +}; + +export { createFlag, isErrorResponse }; diff --git a/src/actions/space/index.js b/src/actions/space/index.js index eb6ed03a..b19c80c4 100644 --- a/src/actions/space/index.js +++ b/src/actions/space/index.js @@ -5,7 +5,7 @@ import { FLAG_GETTING_SPACES, FLAG_LOADING_SPACE, CLEAR_SPACE, - ON_GET_SPACE_SUCCESS, + GET_SPACE_SUCCEEDED, FLAG_EXPORTING_SPACE, FLAG_DELETING_SPACE, ON_SPACE_DELETED, @@ -33,64 +33,98 @@ import { } from '../../config/channels'; import { ERROR_DELETING_MESSAGE, - ERROR_DOWNLOADING_MESSAGE, + // ERROR_DOWNLOADING_MESSAGE, ERROR_EXPORTING_MESSAGE, + ERROR_GETTING_SPACE_MESSAGE, ERROR_JSON_CORRUPTED_MESSAGE, ERROR_SPACE_AVAILABLE_MESSAGE, ERROR_ZIP_CORRUPTED_MESSAGE, SUCCESS_DELETING_MESSAGE, - SUCCESS_EXPORTING_MESSAGE, SUCCESS_SPACE_LOADED_MESSAGE + SUCCESS_EXPORTING_MESSAGE, + SUCCESS_SPACE_LOADED_MESSAGE, } from '../../config/messages'; +import { createFlag, isErrorResponse } from '../common'; +import { generateGetSpaceEndpoint } from '../../config/endpoints'; +import { DEFAULT_GET_REQUEST } from '../../config/rest'; -const flagGettingSpace = flag => dispatch => dispatch({ - type: FLAG_GETTING_SPACE, - payload: flag, -}); - -const flagGettingSpaces = flag => dispatch => dispatch({ - type: FLAG_GETTING_SPACES, - payload: flag, -}); - -const flagLoadingSpace = flag => dispatch => dispatch({ - type: FLAG_LOADING_SPACE, - payload: flag, -}); - -const flagDeletingSpace = flag => dispatch => dispatch({ - type: FLAG_DELETING_SPACE, - payload: flag, -}); - -const flagExportingSpace = flag => dispatch => dispatch({ - type: FLAG_EXPORTING_SPACE, - payload: flag, -}); - -const getSpace = async ({ id, spaces }) => async (dispatch) => { - // raise flag - dispatch(flagGettingSpace(true)); - // tell electron to download space - window.ipcRenderer.send(GET_SPACE_CHANNEL, { id, spaces }); - // create listener - window.ipcRenderer.once(GET_SPACE_CHANNEL, (event, space) => { - // dispatch that the getter has succeeded - if (space === ERROR_GENERAL) { - toastr.error('Error', ERROR_DOWNLOADING_MESSAGE); - } else { - dispatch({ - type: ON_GET_SPACE_SUCCESS, - payload: space, - }); - } +const flagGettingSpace = createFlag(FLAG_GETTING_SPACE); +const flagGettingSpaces = createFlag(FLAG_GETTING_SPACES); +const flagLoadingSpace = createFlag(FLAG_LOADING_SPACE); +const flagDeletingSpace = createFlag(FLAG_DELETING_SPACE); +const flagExportingSpace = createFlag(FLAG_EXPORTING_SPACE); + +// const getSpace = async ({ id, spaces }) => async (dispatch) => { +// // raise flag +// dispatch(flagGettingSpace(true)); +// // tell electron to download space +// window.ipcRenderer.send(GET_SPACE_CHANNEL, { id, spaces }); +// // create listener +// window.ipcRenderer.once(GET_SPACE_CHANNEL, (event, space) => { +// // dispatch that the getter has succeeded +// if (space === ERROR_GENERAL) { +// toastr.error('Error', ERROR_DOWNLOADING_MESSAGE); +// } else { +// dispatch({ +// type: GET_SPACE_SUCCEEDED, +// payload: space, +// }); +// } +// dispatch(flagGettingSpace(false)); +// }); +// // lower flag +// // // delete the listener +// // }); +// }; + +const getLocalSpace = async ({ id }) => async dispatch => { + try { + dispatch(flagGettingSpace(true)); + + // tell electron to get space + window.ipcRenderer.send(GET_SPACE_CHANNEL, { id }); + + // create listener + window.ipcRenderer.once(GET_SPACE_CHANNEL, async (event, space) => { + // if there is no space offline, show error + if (!space) { + toastr.error('Error', ERROR_GETTING_SPACE_MESSAGE); + } else { + dispatch({ + type: GET_SPACE_SUCCEEDED, + payload: space, + }); + } + }); + } catch (err) { + toastr.error('Error', ERROR_GETTING_SPACE_MESSAGE); + } finally { dispatch(flagGettingSpace(false)); - }); - // lower flag - // // delete the listener - // }); + } +}; + +const getRemoteSpace = async ({ id }) => async dispatch => { + try { + dispatch(flagGettingSpace(true)); + + const url = generateGetSpaceEndpoint(id); + const response = await fetch(url, DEFAULT_GET_REQUEST); + + // throws if it is an error + await isErrorResponse(response); + + const space = await response.json(); + dispatch({ + type: GET_SPACE_SUCCEEDED, + payload: space, + }); + } catch (err) { + toastr.error('Error', ERROR_GETTING_SPACE_MESSAGE); + } finally { + dispatch(flagGettingSpace(false)); + } }; -const getSpaces = () => (dispatch) => { +const getSpaces = () => dispatch => { dispatch(flagGettingSpaces(true)); window.ipcRenderer.send(GET_SPACES_CHANNEL); // create listener @@ -104,23 +138,30 @@ const getSpaces = () => (dispatch) => { }); }; -const clearSpace = () => (dispatch) => { +const clearSpace = () => dispatch => { dispatch(clearPhase()); return dispatch({ type: CLEAR_SPACE, }); }; -const exportSpace = (id, spaces, spaceTitle) => (dispatch) => { +const exportSpace = (id, spaces, spaceName) => dispatch => { dispatch(flagExportingSpace(true)); - window.ipcRenderer.send(SHOW_SAVE_DIALOG_CHANNEL, spaceTitle); - window.ipcRenderer.once(SAVE_DIALOG_PATH_SELECTED_CHANNEL, (event, archivePath) => { - if (archivePath) { - window.ipcRenderer.send(EXPORT_SPACE_CHANNEL, { archivePath, id, spaces }); - } else { - dispatch(flagExportingSpace(false)); + window.ipcRenderer.send(SHOW_SAVE_DIALOG_CHANNEL, spaceName); + window.ipcRenderer.once( + SAVE_DIALOG_PATH_SELECTED_CHANNEL, + (event, archivePath) => { + if (archivePath) { + window.ipcRenderer.send(EXPORT_SPACE_CHANNEL, { + archivePath, + id, + spaces, + }); + } else { + dispatch(flagExportingSpace(false)); + } } - }); + ); window.ipcRenderer.once(EXPORTED_SPACE_CHANNEL, (event, newSpaces) => { switch (newSpaces) { case ERROR_GENERAL: @@ -133,7 +174,7 @@ const exportSpace = (id, spaces, spaceTitle) => (dispatch) => { }); }; -const deleteSpace = ({ id }) => (dispatch) => { +const deleteSpace = ({ id }) => dispatch => { dispatch(flagDeletingSpace(true)); window.ipcRenderer.send(SHOW_MESSAGE_DIALOG_CHANNEL); window.ipcRenderer.once(MESSAGE_DIALOG_RESPOND_CHANNEL, (event, respond) => { @@ -159,8 +200,7 @@ const deleteSpace = ({ id }) => (dispatch) => { }); }; - -const loadSpace = ({ fileLocation }) => (dispatch) => { +const loadSpace = ({ fileLocation }) => dispatch => { dispatch(flagLoadingSpace(true)); window.ipcRenderer.send(LOAD_SPACE_CHANNEL, { fileLocation }); window.ipcRenderer.once(LOADED_SPACE_CHANNEL, (event, newSpaces) => { @@ -185,6 +225,14 @@ const loadSpace = ({ fileLocation }) => (dispatch) => { }); }; +const getSpace = ({ id }) => dispatch => { + if (window.navigator.onLine) { + dispatch(getRemoteSpace({ id })); + } else { + dispatch(getLocalSpace({ id })); + } +}; + // const saveSpace = async () => async (dispatch) => { // // download all clips si that they are easily fetched later // try { @@ -210,6 +258,8 @@ export { clearSpace, deleteSpace, exportSpace, - getSpace, + getRemoteSpace, + getLocalSpace, getSpaces, + getSpace, }; diff --git a/src/components/VisitSpace.js b/src/components/VisitSpace.js index d46444a3..794b281b 100644 --- a/src/components/VisitSpace.js +++ b/src/components/VisitSpace.js @@ -1,8 +1,10 @@ import React, { Component } from 'react'; +import { toastr } from 'react-redux-toastr'; import PropTypes from 'prop-types'; import { withRouter } from 'react-router'; import classNames from 'classnames'; import { withStyles } from '@material-ui/core/styles'; +import { connect } from 'react-redux'; import Drawer from '@material-ui/core/Drawer'; import CssBaseline from '@material-ui/core/CssBaseline'; import AppBar from '@material-ui/core/AppBar'; @@ -22,6 +24,9 @@ import SearchIcon from '@material-ui/icons/Search'; import Language from '@material-ui/icons/Language'; import Publish from '@material-ui/icons/Publish'; import SettingsIcon from '@material-ui/icons/Settings'; +import FormControl from '@material-ui/core/FormControl'; +import Input from '@material-ui/core/Input'; +import Button from '@material-ui/core/Button'; import Styles from '../Styles'; import { HOME_PATH, @@ -29,20 +34,31 @@ import { SEARCH_SPACE_PATH, SETTINGS_PATH, } from '../config/paths'; +import Loader from './LoadSpace'; +import { + ERROR_MESSAGE_HEADER, + OFFLINE_ERROR_MESSAGE, +} from '../config/messages'; class VisitSpace extends Component { state = { open: false, + spaceId: '', }; static propTypes = { classes: PropTypes.shape({}).isRequired, theme: PropTypes.shape({}).isRequired, + activity: PropTypes.bool, history: PropTypes.shape({ replace: PropTypes.func.isRequired, }).isRequired, }; + static defaultProps = { + activity: false, + }; + handleDrawerOpen = () => { this.setState({ open: true }); }; @@ -72,9 +88,42 @@ class VisitSpace extends Component { } }; + handleChangeSpaceId = event => { + const spaceId = event.target.value; + this.setState({ spaceId }); + }; + + handleClick = () => { + const { history } = this.props; + const { spaceId: id } = this.state; + if (!window.navigator.onLine) { + return toastr.error(ERROR_MESSAGE_HEADER, OFFLINE_ERROR_MESSAGE); + } + if (id && id !== '') { + const { replace } = history; + return replace(`/space/${id}`); + } + return false; + }; + render() { - const { classes, theme } = this.props; - const { open } = this.state; + const { classes, theme, activity } = this.props; + const { open, spaceId } = this.state; + + if (activity) { + return ( +
+ + + + +
+ +
+
+ ); + } + return (
@@ -153,13 +202,48 @@ class VisitSpace extends Component { })} >
- - VisitSpace - + + + Visit a Space + + + +
); } } -export default withRouter(withStyles(Styles, { withTheme: true })(VisitSpace)); +const mapStateToProps = ({ Space }) => ({ + activity: Space.get('current').get('activity'), +}); + +const ConnectedComponent = connect(mapStateToProps)(VisitSpace); +const StyledComponent = withStyles(Styles, { withTheme: true })( + ConnectedComponent +); +export default withRouter(StyledComponent); diff --git a/src/components/common/MediaCard.js b/src/components/common/MediaCard.js index aab4e42e..19f92177 100644 --- a/src/components/common/MediaCard.js +++ b/src/components/common/MediaCard.js @@ -20,39 +20,25 @@ const styles = theme => ({ }, }); -const MediaCard = (props) => { - const { - classes, - title, - image, - text, - button, - } = props; +const MediaCard = props => { + const { classes, name, image, text, button } = props; return ( - + - {title} - - - {text} + {name} + {text} - - {button} - + {button} ); }; MediaCard.propTypes = { classes: PropTypes.shape({ media: PropTypes.string.isRequired }).isRequired, - title: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, image: PropTypes.string.isRequired, text: PropTypes.string.isRequired, button: PropTypes.shape(Button).isRequired, diff --git a/src/components/phase/PhaseImage.js b/src/components/phase/PhaseImage.js index f3067563..85a3448b 100644 --- a/src/components/phase/PhaseImage.js +++ b/src/components/phase/PhaseImage.js @@ -2,14 +2,28 @@ import React from 'react'; import PropTypes from 'prop-types'; import './PhaseImage.css'; -const PhaseImage = ({ uri }) => ( -
- No alt -
-); +const PhaseImage = ({ url, asset, name }) => { + let uri = url; + if (asset) { + uri = `file://${asset}`; + } + return ( +
+ {name} +
+ ); +}; PhaseImage.propTypes = { - uri: PropTypes.string.isRequired, + url: PropTypes.string, + asset: PropTypes.string, + name: PropTypes.string, +}; + +PhaseImage.defaultProps = { + url: null, + asset: null, + name: 'Image', }; export default PhaseImage; diff --git a/src/components/phase/PhaseItems.js b/src/components/phase/PhaseItems.js index 44ec0511..7a982027 100644 --- a/src/components/phase/PhaseItems.js +++ b/src/components/phase/PhaseItems.js @@ -3,66 +3,47 @@ import { TEXT, IMAGE, VIDEO, - LAB, - APP, + RESOURCE, + APPLICATION, + IFRAME, } from '../../config/constants'; import PhaseText from './PhaseText'; import PhaseImage from './PhaseImage'; import PhaseVideo from './PhaseVideo'; -import PhaseLab from './PhaseLab'; import PhaseApp from './PhaseApp'; -const renderPhaseItem = (item) => { - const { id, type, content } = item; - switch (type) { - case TEXT: - return ( - - ); - case IMAGE: - return ( - - ); - case LAB: - return ( - - ); - case APP: - return ( - - ); - case VIDEO: - return ( - - ); +const renderResource = item => { + const { id, mimeType, content, asset, url, name } = item; + if (mimeType === TEXT) { + return ; + } + + if (IMAGE.test(mimeType)) { + return ; + } + + if (VIDEO.test(mimeType)) { + return ; + } + + if (mimeType === IFRAME) { + return null; + } + + return
; +}; + +const renderPhaseItem = item => { + const { id, category, asset } = item; + switch (category) { + case RESOURCE: + return renderResource(item); + + case APPLICATION: + return ; default: - return ( -
- ); + return
; } }; diff --git a/src/components/space/SavedSpaces.js b/src/components/space/SavedSpaces.js index 9f75260a..ae8f6a6b 100644 --- a/src/components/space/SavedSpaces.js +++ b/src/components/space/SavedSpaces.js @@ -18,41 +18,35 @@ class SavedSpaces extends Component { } render() { - const { - spaces, - classes, - history, - } = this.props; - const MediaCards = spaces.map((space) => { - const { - id, - title, - image, - text, - asset, - } = space; + const { spaces, classes, history } = this.props; + const MediaCards = spaces.map(space => { + const { id, name, image, text, asset } = space; const { replace } = history; + const ViewButton = ( + + ); return ( replace(`/space/${id}`)} - > - - View - - )} + button={ViewButton} /> ); @@ -78,9 +72,18 @@ const mapDispatchToProps = { SavedSpaces.propTypes = { classes: PropTypes.shape({ appBar: PropTypes.string.isRequired }).isRequired, - spaces: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string.isRequired })).isRequired, + spaces: PropTypes.arrayOf( + PropTypes.shape({ id: PropTypes.string.isRequired }) + ).isRequired, history: PropTypes.shape({ length: PropTypes.number.isRequired }).isRequired, dispatchClearSpace: PropTypes.func.isRequired, }; -export default withRouter(withStyles({})(connect(null, mapDispatchToProps)(SavedSpaces))); +export default withRouter( + withStyles({})( + connect( + null, + mapDispatchToProps + )(SavedSpaces) + ) +); diff --git a/src/components/space/SpaceDescription.js b/src/components/space/SpaceDescription.js index c208d6cd..61ab1eb9 100644 --- a/src/components/space/SpaceDescription.js +++ b/src/components/space/SpaceDescription.js @@ -5,17 +5,19 @@ import { withStyles } from '@material-ui/core'; import Styles from '../../Styles'; import './SpaceDescription.css'; -const SpaceDescription = ({ - description, - classes, - start, -}) => ( +const SpaceDescription = ({ description, classes, start }) => (

-
diff --git a/src/components/space/SpaceNotFound.js b/src/components/space/SpaceNotFound.js new file mode 100644 index 00000000..4342d2e7 --- /dev/null +++ b/src/components/space/SpaceNotFound.js @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withRouter } from 'react-router'; +import Button from '@material-ui/core//Button'; + +const SpaceNotFound = ({ history: { replace } }) => { + return ( +
+ Space not found. + +
+ ); +}; + +SpaceNotFound.propTypes = { + history: PropTypes.shape({ length: PropTypes.number.isRequired }).isRequired, +}; + +export default withRouter(SpaceNotFound); diff --git a/src/components/space/SpaceScreen.js b/src/components/space/SpaceScreen.js index a7a24674..82c2581a 100644 --- a/src/components/space/SpaceScreen.js +++ b/src/components/space/SpaceScreen.js @@ -21,9 +21,10 @@ import Language from '@material-ui/icons/Language'; import Publish from '@material-ui/icons/Publish'; import SettingsIcon from '@material-ui/icons/Settings'; import { withStyles } from '@material-ui/core'; +import { withRouter } from 'react-router'; import Loader from '../common/Loader'; import PhaseComponent from '../phase/Phase'; -import { selectPhase, getSpace, clearPhase, clearSpace } from '../../actions'; +import { selectPhase, clearPhase, clearSpace, getSpace } from '../../actions'; import './SpaceScreen.css'; import Styles from '../../Styles'; import { @@ -34,6 +35,7 @@ import { VISIT_PATH, } from '../../config/paths'; import SpaceHeader from './SpaceHeader'; +import SpaceNotFound from './SpaceNotFound'; class SpaceScreen extends Component { state = { @@ -143,6 +145,7 @@ class SpaceScreen extends Component { render() { const { space, phase, activity, classes, theme } = this.props; const { openDrawer, selected } = this.state; + if (activity) { return (
@@ -157,7 +160,7 @@ class SpaceScreen extends Component { ); } if (!space || space.isEmpty()) { - return

Space not found.

; + return ; } const name = space.get('name'); const phases = space.get('phases'); @@ -283,9 +286,13 @@ const mapDispatchToProps = { dispatchClearSpace: clearSpace, }; -export default withStyles(Styles, { withTheme: true })( - connect( - mapStateToProps, - mapDispatchToProps - )(SpaceScreen) +const ConnectedComponent = connect( + mapStateToProps, + mapDispatchToProps +)(SpaceScreen); + +const StyledComponent = withStyles(Styles, { withTheme: true })( + ConnectedComponent ); + +export default withRouter(StyledComponent); diff --git a/src/config/constants.js b/src/config/constants.js index 20b57c1d..67b8ab5b 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -1,6 +1,7 @@ // phase item types -export const TEXT = 'text'; -export const VIDEO = 'video'; -export const IMAGE = 'image'; -export const LAB = 'lab'; -export const APP = 'app'; +export const TEXT = 'text/html'; +export const VIDEO = new RegExp('video/*'); +export const IMAGE = new RegExp('image/*'); +export const RESOURCE = 'Resource'; +export const APPLICATION = 'Application'; +export const IFRAME = 'application/octet-stream'; diff --git a/src/config/endpoints.js b/src/config/endpoints.js new file mode 100644 index 00000000..ce11828e --- /dev/null +++ b/src/config/endpoints.js @@ -0,0 +1,5 @@ +const { REACT_APP_GRAASP_HOST } = process.env; + +// eslint-disable-next-line import/prefer-default-export +export const generateGetSpaceEndpoint = id => + `${REACT_APP_GRAASP_HOST}/spaces/${id}/download-offline`; diff --git a/src/config/messages.js b/src/config/messages.js index d53b3691..31c377e3 100644 --- a/src/config/messages.js +++ b/src/config/messages.js @@ -1,10 +1,19 @@ - -export const ERROR_DOWNLOADING_MESSAGE = 'There was a problem downloading your files'; -export const ERROR_EXPORTING_MESSAGE = 'There was a problem exporting this space.'; +export const ERROR_DOWNLOADING_MESSAGE = + 'There was a problem downloading your files'; +export const ERROR_EXPORTING_MESSAGE = + 'There was a problem exporting this space.'; export const SUCCESS_EXPORTING_MESSAGE = 'Space was exported successfully'; export const ERROR_DELETING_MESSAGE = 'There was a problem deleting this space'; export const SUCCESS_DELETING_MESSAGE = 'Space was deleted successfully'; -export const ERROR_ZIP_CORRUPTED_MESSAGE = 'The archive provided is not formatted properly'; -export const ERROR_JSON_CORRUPTED_MESSAGE = 'Space\'s Jon file is corrupted'; -export const ERROR_SPACE_AVAILABLE_MESSAGE = 'A space with the same id is already available'; +export const ERROR_ZIP_CORRUPTED_MESSAGE = + 'The archive provided is not formatted properly'; +export const ERROR_JSON_CORRUPTED_MESSAGE = "Space's Jon file is corrupted"; +export const ERROR_SPACE_AVAILABLE_MESSAGE = + 'A space with the same id is already available'; export const SUCCESS_SPACE_LOADED_MESSAGE = 'Space was loaded successfully!'; +export const UNEXPECTED_ERROR_MESSAGE = 'An unexpected error has occurred.'; +export const ERROR_GETTING_SPACE_MESSAGE = + 'There was an error getting that space.'; +export const OFFLINE_ERROR_MESSAGE = + 'This functionality requires online connectivity.'; +export const ERROR_MESSAGE_HEADER = 'Error'; diff --git a/src/config/rest.js b/src/config/rest.js new file mode 100644 index 00000000..4cf5f8a0 --- /dev/null +++ b/src/config/rest.js @@ -0,0 +1,21 @@ +// request defaults +const DEFAULT_REQUEST = { + headers: { 'content-type': 'application/json' }, + credentials: 'include', +}; +export const DEFAULT_GET_REQUEST = { + ...DEFAULT_REQUEST, + method: 'GET', +}; +export const DEFAULT_POST_REQUEST = { + ...DEFAULT_REQUEST, + method: 'POST', +}; +export const DEFAULT_PATCH_REQUEST = { + ...DEFAULT_REQUEST, + method: 'PATCH', +}; +export const DEFAULT_DELETE_REQUEST = { + ...DEFAULT_REQUEST, + method: 'DELETE', +}; diff --git a/src/reducers/SpaceReducer.js b/src/reducers/SpaceReducer.js index 2b964bc2..784ae3e3 100644 --- a/src/reducers/SpaceReducer.js +++ b/src/reducers/SpaceReducer.js @@ -4,7 +4,7 @@ import { TOGGLE_SPACE_MENU, GET_SPACES, CLEAR_SPACE, - ON_GET_SPACE_SUCCESS, + GET_SPACE_SUCCEEDED, FLAG_GETTING_SPACE, FLAG_DELETING_SPACE, FLAG_GETTING_SPACES, @@ -51,7 +51,7 @@ export default (state = INITIAL_STATE, { type, payload }) => { return state.setIn(['current', 'content'], Immutable.Map(payload)); case TOGGLE_SPACE_MENU: return state.setIn(['current', 'menu', 'open'], payload); - case ON_GET_SPACE_SUCCESS: + case GET_SPACE_SUCCEEDED: return state.setIn(['current', 'content'], Immutable.Map(payload)); default: return state; diff --git a/src/types/space/index.js b/src/types/space/index.js index ad49f800..90d7affb 100644 --- a/src/types/space/index.js +++ b/src/types/space/index.js @@ -7,5 +7,5 @@ export const FLAG_DELETING_SPACE = 'FLAG_DELETING_SPACE'; export const FLAG_EXPORTING_SPACE = 'FLAG_EXPORTING_SPACES'; export const TOGGLE_SPACE_MENU = 'TOGGLE_SPACE_MENU'; export const CLEAR_SPACE = 'CLEAR_SPACE'; -export const ON_GET_SPACE_SUCCESS = 'ON_GET_SPACE_SUCCESS'; +export const GET_SPACE_SUCCEEDED = 'GET_SPACE_SUCCEEDED'; export const ON_SPACE_DELETED = 'ON_SPACE_DELETED'; diff --git a/yarn.lock b/yarn.lock index 906a6b14..d28ba713 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11140,6 +11140,11 @@ react-app-polyfill@^1.0.0: regenerator-runtime "0.13.2" whatwg-fetch "3.0.0" +react-detect-offline@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/react-detect-offline/-/react-detect-offline-2.3.0.tgz#7ff517a85d549ca27a70b3631ca5b18d95d78a06" + integrity sha512-PouVMhwCu2PxFe0yzlLa4ChObmjP6bpV0Mm18x98zMR5VRNwBUYQ6KuBgLiKHc7aDD7JpI2QoN0etF7kGXPYCg== + react-dev-utils@9.0.0, react-dev-utils@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-9.0.0.tgz#356d95db442441c5d4748e0e49f4fd1e71aecbbd"