diff --git a/src/components/alerts/alert.css b/src/components/alerts/alert.css new file mode 100644 index 0000000000000000000000000000000000000000..3bc1c932c723bc55fdde203b07e5d32f4f127724 --- /dev/null +++ b/src/components/alerts/alert.css @@ -0,0 +1,28 @@ +@import "../../css/units.css"; +@import "../../css/colors.css"; +@import "../../css/z-index.css"; + +.alert { + width: 100%; + background: #FFF0DF; + display: flex; + flex-direction: row; + overflow: hidden; + align-items: left; + border: 1px solid #FF8C1A; + border-radius: 8px; + padding: 12px; + box-shadow: 2px 2px 2px 2px rgba(255, 140, 26, 0.25); +} + +.alert-message { + color: #555; + font-weight: bold; + font-size: 12px; + line-height: 22pt; + width: 100%; +} + +.alert-remove-button { + color: #FF8C1A; +} diff --git a/src/components/alerts/alerts.jsx b/src/components/alerts/alerts.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e467c05e8a644d23de76c4f98b7f7f0aa26db35c --- /dev/null +++ b/src/components/alerts/alerts.jsx @@ -0,0 +1,41 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import Box from '../box/box.jsx'; +import Button from '../button/button.jsx'; + +import styles from './alert.css'; + +const Alerts = ({ + className, + message, + onCloseAlert +}) => ( + <Box + bounds="parent" + className={classNames(className)} + > + <Box + className={styles.alert} + > + <div className={styles.alertMessage}> + {message} + </div> + <Button + className={styles.alertRemoveButton} + onClick={onCloseAlert} + > + { /* eslint-disable react/jsx-no-literals */ } + x + </Button> + </Box> + </Box> +); + +Alerts.propTypes = { + className: PropTypes.string, + message: PropTypes.string.isRequired, + onCloseAlert: PropTypes.func.isRequired +}; + +export default Alerts; diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index 086011f4ba0a975499f0f7892fcc2eaa3bcee5b5..16f79ea4d684308cf8a6095bb4246b7966eb87af 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -278,3 +278,15 @@ $fade-out-distance: 15px; .extension-button > div { margin-top: 0; } + +/* Alerts */ + +.alerts-container { + width: 448px; + z-index: $z-index-alerts; + left: 0; + right: 0; + margin: auto; + position: absolute; + margin-top: 53px; +} diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index aa1dfc0393388111d80c4179e21b7a0d62fa95f1..76ef997d89ba1d73a971cb10fffc4fa4e9c0242b 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -27,6 +27,7 @@ import ImportModal from '../../containers/import-modal.jsx'; import WebGlModal from '../../containers/webgl-modal.jsx'; import TipsLibrary from '../../containers/tips-library.jsx'; import Cards from '../../containers/cards.jsx'; +import Alerts from '../../containers/alerts.jsx'; import DragLayer from '../../containers/drag-layer.jsx'; import layout, {STAGE_SIZE_MODES} from '../../lib/layout-constants'; @@ -53,6 +54,7 @@ let isRendererSupported = null; const GUIComponent = props => { const { activeTabIndex, + alertsVisible, basePath, backdropLibraryVisible, backpackOptions, @@ -108,7 +110,11 @@ const GUIComponent = props => { isRendererSupported={isRendererSupported} stageSize={stageSize} vm={vm} - /> + > + {alertsVisible ? ( + <Alerts className={styles.alertsContainer} /> + ) : null} + </StageWrapper> ) : ( <Box className={styles.pageWrapper} @@ -133,6 +139,9 @@ const GUIComponent = props => { {cardsVisible ? ( <Cards /> ) : null} + {alertsVisible ? ( + <Alerts className={styles.alertsContainer} /> + ) : null} {costumeLibraryVisible ? ( <CostumeLibrary vm={vm} diff --git a/src/containers/alerts.jsx b/src/containers/alerts.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c47d956dea31b7021a5f27fb26ed8612558fca1e --- /dev/null +++ b/src/containers/alerts.jsx @@ -0,0 +1,23 @@ +import {connect} from 'react-redux'; + +import { + showAlert, + closeAlert +} from '../reducers/alerts'; + +import AlertsComponent from '../components/alerts/alerts.jsx'; + +const mapStateToProps = state => ({ + visible: state.scratchGui.alerts.visible, + message: state.scratchGui.alerts.message +}); + +const mapDispatchToProps = dispatch => ({ + onShowAlert: () => dispatch(showAlert()), + onCloseAlert: () => dispatch(closeAlert()) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(AlertsComponent); diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index 04c6142dce25e073c4f0a0676cd2ab3e5c37590d..6ef14439ba7c19b2f39301e15e3154fc59dd25b8 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -125,6 +125,7 @@ GUI.propTypes = { const mapStateToProps = (state, ownProps) => ({ activeTabIndex: state.scratchGui.editorTab.activeTabIndex, + alertsVisible: state.scratchGui.alerts.visible, backdropLibraryVisible: state.scratchGui.modals.backdropLibrary, blocksTabVisible: state.scratchGui.editorTab.activeTabIndex === BLOCKS_TAB_INDEX, cardsVisible: state.scratchGui.cards.visible, diff --git a/src/css/z-index.css b/src/css/z-index.css index 26893f08fa5147930328992359d709777bef0ec7..87cc2cd01a70bc8cc35a88f6019b6744040a26e3 100644 --- a/src/css/z-index.css +++ b/src/css/z-index.css @@ -23,6 +23,7 @@ $z-index-stage-color-picker-background: 2000; $z-index-stage-with-color-picker: 2010; $z-index-stage-header: 5000; $z-index-stage-wrapper-overlay: 5000; +$z-index-alerts: 5010; /* in most interfaces, the context menu is always on top */ $z-index-context-menu: 10000; diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx index 67e70f90019e90b92c262fdef5067c12c9c927ff..cf2307726c49e3a53a246119ae623b07e458cef6 100644 --- a/src/lib/vm-listener-hoc.jsx +++ b/src/lib/vm-listener-hoc.jsx @@ -9,6 +9,7 @@ import {updateTargets} from '../reducers/targets'; import {updateBlockDrag} from '../reducers/block-drag'; import {updateMonitors} from '../reducers/monitors'; import {setRunningState, setTurboState} from '../reducers/vm-status'; +import {showAlert} from '../reducers/alerts'; /* * Higher Order Component to manage events emitted by the VM @@ -36,6 +37,7 @@ const vmListenerHOC = function (WrappedComponent) { this.props.vm.on('TURBO_MODE_OFF', this.props.onTurboModeOff); this.props.vm.on('PROJECT_RUN_START', this.props.onProjectRunStart); this.props.vm.on('PROJECT_RUN_STOP', this.props.onProjectRunStop); + this.props.vm.on('PERIPHERAL_ERROR', this.props.onShowAlert); } componentDidMount () { if (this.props.attachKeyboardEvents) { @@ -93,6 +95,7 @@ const vmListenerHOC = function (WrappedComponent) { onProjectRunStop, onTurboModeOff, onTurboModeOn, + onShowAlert, /* eslint-enable no-unused-vars */ ...props } = this.props; @@ -107,6 +110,7 @@ const vmListenerHOC = function (WrappedComponent) { onMonitorsUpdate: PropTypes.func.isRequired, onProjectRunStart: PropTypes.func.isRequired, onProjectRunStop: PropTypes.func.isRequired, + onShowAlert: PropTypes.func.isRequired, onTargetsUpdate: PropTypes.func.isRequired, onTurboModeOff: PropTypes.func.isRequired, onTurboModeOn: PropTypes.func.isRequired, @@ -134,7 +138,10 @@ const vmListenerHOC = function (WrappedComponent) { onProjectRunStart: () => dispatch(setRunningState(true)), onProjectRunStop: () => dispatch(setRunningState(false)), onTurboModeOn: () => dispatch(setTurboState(true)), - onTurboModeOff: () => dispatch(setTurboState(false)) + onTurboModeOff: () => dispatch(setTurboState(false)), + onShowAlert: () => { + dispatch(showAlert('Scratch has lost connection to peripheral.')); + } }); return connect( mapStateToProps, diff --git a/src/reducers/alerts.js b/src/reducers/alerts.js new file mode 100644 index 0000000000000000000000000000000000000000..bc152e38f380f93969e34ebe68dadf1c9dead572 --- /dev/null +++ b/src/reducers/alerts.js @@ -0,0 +1,42 @@ +const CLOSE_ALERT = 'scratch-gui/alerts/CLOSE_ALERT'; +const SHOW_ALERT = 'scratch-gui/alerts/SHOW_ALERT'; + +const initialState = { + message: '', + visible: false +}; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case SHOW_ALERT: + return Object.assign({}, state, { + visible: true, + message: action.message + }); + case CLOSE_ALERT: + return Object.assign({}, state, { + visible: false + }); + default: + return state; + } +}; + +const closeAlert = function () { + return {type: CLOSE_ALERT}; +}; + +const showAlert = function (message) { + return { + type: SHOW_ALERT, + message + }; +}; + +export { + reducer as default, + initialState as alertsInitialState, + closeAlert, + showAlert +}; diff --git a/src/reducers/gui.js b/src/reducers/gui.js index 8869c1a35031e1d46b8dd28f4f408d2500946840..4d6cea9c0ea151d29bf92945ae10d61e59c8dbf8 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -1,4 +1,5 @@ import {applyMiddleware, compose, combineReducers} from 'redux'; +import alertsReducer, {alertsInitialState} from './alerts'; import assetDragReducer, {assetDragInitialState} from './asset-drag'; import cardsReducer, {cardsInitialState} from './cards'; import colorPickerReducer, {colorPickerInitialState} from './color-picker'; @@ -26,6 +27,7 @@ import decks from '../lib/libraries/decks/index.jsx'; const guiMiddleware = compose(applyMiddleware(throttle(300, {leading: true, trailing: true}))); const guiInitialState = { + alerts: alertsInitialState, assetDrag: assetDragInitialState, blockDrag: blockDragInitialState, cards: cardsInitialState, @@ -91,6 +93,7 @@ const initTutorialCard = function (currentState, deckId) { }; const guiReducer = combineReducers({ + alerts: alertsReducer, assetDrag: assetDragReducer, blockDrag: blockDragReducer, cards: cardsReducer,