From 954e74be050f3f41eb89afb67eae05b314574af4 Mon Sep 17 00:00:00 2001 From: Karishma Chadha <kchadha@scratch.mit.edu> Date: Tue, 20 Feb 2018 16:30:56 -0500 Subject: [PATCH] Initial SB2 Import UI (button in preview modal + import modal that can load projects via url). Preliminary validation on url, still needs error handling. --- src/components/gui/gui.jsx | 7 + src/components/import-modal/import-modal.css | 200 ++++++++++++++++++ src/components/import-modal/import-modal.jsx | 132 ++++++++++++ .../preview-modal/preview-modal.css | 9 +- .../preview-modal/preview-modal.jsx | 11 + src/containers/gui.jsx | 2 + src/containers/import-modal.jsx | 112 ++++++++++ src/containers/preview-modal.jsx | 17 +- src/css/colors.css | 2 + src/reducers/modals.js | 19 +- 10 files changed, 503 insertions(+), 8 deletions(-) create mode 100644 src/components/import-modal/import-modal.css create mode 100644 src/components/import-modal/import-modal.jsx create mode 100644 src/containers/import-modal.jsx diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 488e705e8..5c7a378dc 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -19,6 +19,7 @@ import Box from '../box/box.jsx'; import FeedbackForm from '../feedback-form/feedback-form.jsx'; import MenuBar from '../menu-bar/menu-bar.jsx'; import PreviewModal from '../../containers/preview-modal.jsx'; +import ImportModal from '../../containers/import-modal.jsx'; import WebGlModal from '../../containers/webgl-modal.jsx'; import layout from '../../lib/layout-constants.js'; @@ -41,6 +42,7 @@ const GUIComponent = props => { children, costumesTabVisible, feedbackFormVisible, + importInfoVisible, intl, onExtensionButtonClick, onActivateTab, @@ -76,6 +78,9 @@ const GUIComponent = props => { {previewInfoVisible ? ( <PreviewModal /> ) : null} + {importInfoVisible ? ( + <ImportModal /> + ) : null} {feedbackFormVisible ? ( <FeedbackForm /> ) : null} @@ -169,9 +174,11 @@ GUIComponent.propTypes = { children: PropTypes.node, costumesTabVisible: PropTypes.bool, feedbackFormVisible: PropTypes.bool, + importInfoVisible: PropTypes.bool, intl: intlShape.isRequired, onActivateTab: PropTypes.func, onExtensionButtonClick: PropTypes.func, + onTabSelect: PropTypes.func, previewInfoVisible: PropTypes.bool, soundsTabVisible: PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired diff --git a/src/components/import-modal/import-modal.css b/src/components/import-modal/import-modal.css new file mode 100644 index 000000000..80a2de576 --- /dev/null +++ b/src/components/import-modal/import-modal.css @@ -0,0 +1,200 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; +@import "../../css/typography.css"; + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; + background-color: hsla(215, 100%, 65%, .9); +} + +.modal-content { + margin: 100px auto; + outline: none; + border: .25rem solid hsla(0, 100%, 100%, .25); + padding: 0; + border-radius: $space; + user-select: none; + width: 500px; + + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + + color: $text-primary; + overflow: hidden; +} + +/* .illustration { + width: 100%; + height: 208px; + background-color: $motion-primary; + background-image: url('./welcome.png'); + background-size: cover; +} */ + +/* .input { + margin-bottom: 1.5rem; + width: 100%; + border: 1px solid rgba(0,0,0,.1); + border-radius: 5px; + padding: 0 1rem; + height: 3rem; + color: #6b6b6b; + font-size: .875rem; +} */ + +/* + Modal header has 3 items: + |filter title x| + + Use the same width for both side item containers, + so that title remains centered +*/ +$sides: 20rem; + +.header { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + height: $library-header-height; + + box-sizing: border-box; + width: 100%; + background-color: $import-primary; + + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 1rem; + font-weight: normal; +} + +.header-item { + display: flex; + align-items: center; + padding: 1rem; + text-decoration: none; + color: white; + user-select: none; +} + +.header-item-filter { + display: flex; + flex-basis: $sides; + justify-content: flex-end; +} + +.header-item-title { + flex-grow: 1; + flex-shrink: 0; + justify-content: center; + user-select: none; + letter-spacing: 0.4px; + cursor: default; +} + +.header-item-close { + display: flex; + flex-basis: $sides; + justify-content: flex-start; +} + +.body { + background: $ui-pane-gray; + padding: 1.5rem 2.25rem; + text-align: center; +} + +.input-row { + margin: 1.5rem 0; + font-weight: bolder; + text-align: right; + display: flex; + justify-content: center; + /*border: 1px solid $import-primary; + border-radius: 0.25rem;*/ +} + +.input-row input { + width: 100%; + padding: 0 1rem; + height: 3rem; + color: #6b6b6b; + font-size: .875rem; +} + +.input-row input::placeholder { + font-style: italic; + color: rgba(87,94,117,0.5); +} + +.input-row input:focus { + outline: none; + border: 1px solid $motion-primary; + border-radius: 0.25rem +} + +.input-row button { + padding: 0.5rem 2rem; + font-weight: bold; + font-size: .875rem; + cursor: pointer; + border: 1px solid $import-primary; + border-radius: 0.25rem; +} + +.input-row button.ok-button { + background: $import-primary; + color: white; +} + +/* .buttonRow input + button { + border: 1px solid $motion-primary; /* rgba(0,0,0,.1); + border-radius: 0.25rem; +} */ + +/* Confirmation buttons at the bottom of the modal */ +.button-row { + margin: 1.5rem 0; + font-weight: bolder; + text-align: right; + display: flex; + justify-content: center; +} + +.button-row button { + padding: 0.5rem 2rem; + background: white; + font-weight: bold; + font-size: .875rem; + cursor: pointer; +} + +.button-row button.no-button { + border: 1px solid $motion-primary; + border-radius: 0.25rem; + color: $motion-primary; +} +.button-row button + button { + margin-left: 0.5rem; +} + +/* .cat-icon { + margin-left: .125rem; + width: 1.5rem; + height: 1.5rem; + vertical-align: middle; +} */ + +.faq-link-text { + margin: 2rem 0 .5rem 0; + font-size: .875rem; + color: $text-primary; +} + +.faq-link { + color: $motion-primary; + text-decoration: none; +} diff --git a/src/components/import-modal/import-modal.jsx b/src/components/import-modal/import-modal.jsx new file mode 100644 index 000000000..9a9ad872d --- /dev/null +++ b/src/components/import-modal/import-modal.jsx @@ -0,0 +1,132 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ReactModal from 'react-modal'; +import Box from '../box/box.jsx'; +import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl'; +import classNames from 'classnames'; + +import CloseButton from '../close-button/close-button.jsx'; + +import styles from './import-modal.css'; + +const messages = defineMessages({ + title: { + id: 'gui.importInfo.title', + defaultMessage: 'View a Scratch 2.0 Project', + description: 'Scratch 2.0 import modal label - for accessibility' + } +}); + +const ImportModal = ({intl, ...props}) => ( + <ReactModal + isOpen + className={styles.modalContent} + contentLabel={intl.formatMessage({...messages.title})} + overlayClassName={styles.modalOverlay} + onRequestClose={props.onCancel} + > + <Box> + <div className={styles.header}> + <div + className={classNames( + styles.headerItem, + styles.headerItemClose + )} + > + <CloseButton + size={CloseButton.SIZE_LARGE} + onClick={props.onCancel} + /> + </div> + <div + className={classNames( + styles.headerItem, + styles.headerItemTitle + )} + > + <h2> + {intl.formatMessage({...messages.title})} + </h2> + </div> + <div className={classNames(styles.headerItem, styles.headerItemFilter)}> + {null} + </div> + </div> + </Box> + + <Box className={styles.body}> + <p> + <FormattedMessage + defaultMessage="Enter a link to one of your shared Scratch projects. Changes made in this 3.0 Preview will not be saved." + description="Import project message" + id="gui.importInfo.message" + /> + </p> + + <Box className={styles.inputRow}> + <input + autoFocus + className={styles.input} + placeholder={props.placeholder} + onChange={props.onChange} + onKeyPress={props.onKeyPress} + /> + <button + className={styles.okButton} + title="viewproject" + onClick={props.onViewProject} + > + <FormattedMessage + defaultMessage="View" + description="Label for button to load a scratch 2.0 project" + id="gui.importModal.viewproject" + /> + </button> + </Box> + <Box className={styles.buttonRow}> + <button + className={styles.noButton} + onClick={props.onCancel} + > + <FormattedMessage + defaultMessage="Go Back" + description="Label for button to back out of importing a project" + id="gui.importInfo.goback" + /> + </button> + </Box> + <Box className={styles.faqLinkText}> + <FormattedMessage + defaultMessage="To learn more, go to the {previewFaqLink}." + description="Invitation to try 3.0 preview" + id="gui.importInfo.previewfaq" + values={{ + previewFaqLink: ( + <a + className={styles.faqLink} + href="//scratch.mit.edu/preview-faq" + > + <FormattedMessage + defaultMessage="Preview FAQ" + description="link to Scratch 3.0 preview FAQ page" + id="gui.importInfo.previewfaqlink" + /> + </a> + ) + }} + /> + </Box> + </Box> + </ReactModal> +); + +ImportModal.propTypes = { + intl: intlShape.isRequired, + onCancel: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onKeyPress: PropTypes.func.isRequired, + onViewProject: PropTypes.func.isRequired, + placeholder: PropTypes.string, +}; + +export default injectIntl(ImportModal); diff --git a/src/components/preview-modal/preview-modal.css b/src/components/preview-modal/preview-modal.css index 858beb5ba..c6e5037e8 100644 --- a/src/components/preview-modal/preview-modal.css +++ b/src/components/preview-modal/preview-modal.css @@ -51,7 +51,7 @@ .button-row button { border: 1px solid $motion-primary; border-radius: 0.25rem; - padding: 0.5rem 2rem; + padding: 0.5rem 1.5rem; background: white; font-weight: bold; font-size: .875rem; @@ -66,6 +66,13 @@ .button-row button.no-button { color: $motion-primary; } + +.button-row button.view-project-button { + background: $import-primary; + border-color: $import-primary; + color: white; +} + .button-row button + button { margin-left: 0.5rem; } diff --git a/src/components/preview-modal/preview-modal.jsx b/src/components/preview-modal/preview-modal.jsx index 71a3b029c..2b9d14407 100644 --- a/src/components/preview-modal/preview-modal.jsx +++ b/src/components/preview-modal/preview-modal.jsx @@ -71,6 +71,17 @@ const PreviewModal = ({intl, ...props}) => ( }} /> </button> + <button + className={styles.viewProjectButton} + title="viewproject" + onClick={props.onViewProject} + > + <FormattedMessage + defaultMessage="View 2.0 Project" + description="Label for button to import a 2.0 project" + id="gui.previewModal.viewproject" + /> + </button> </Box> <Box className={styles.faqLinkText}> <FormattedMessage diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index bae6f9684..de4fe27a2 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -53,6 +53,7 @@ class GUI extends React.Component { GUI.propTypes = { ...GUIComponent.propTypes, feedbackFormVisible: PropTypes.bool, + importInfoVisible: PropTypes.bool, previewInfoVisible: PropTypes.bool, projectData: PropTypes.string, vm: PropTypes.instanceOf(VM) @@ -65,6 +66,7 @@ const mapStateToProps = state => ({ blocksTabVisible: state.editorTab.activeTabIndex === BLOCKS_TAB_INDEX, costumesTabVisible: state.editorTab.activeTabIndex === COSTUMES_TAB_INDEX, feedbackFormVisible: state.modals.feedbackForm, + importInfoVisible: state.modals.importInfo, previewInfoVisible: state.modals.previewInfo, soundsTabVisible: state.editorTab.activeTabIndex === SOUNDS_TAB_INDEX }); diff --git a/src/containers/import-modal.jsx b/src/containers/import-modal.jsx new file mode 100644 index 000000000..9d808b6e3 --- /dev/null +++ b/src/containers/import-modal.jsx @@ -0,0 +1,112 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {connect} from 'react-redux'; +import platform from 'platform'; +// import RegExp from 'regex'; + +import ImportModalComponent from '../components/import-modal/import-modal.jsx'; +import BrowserModalComponent from '../components/browser-modal/browser-modal.jsx'; + +import { + closeImportInfo +} from '../reducers/modals'; + +class ImportModal extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleViewProject', + 'handleCancel', + 'handleKeyPress', + 'handleChange' + ]); + + this.state = { + inputValue: '' + }; + } + handleKeyPress (event) { + if (event.key === 'Enter') this.handleViewProject(); + } + handleViewProject () { + const inputValue = this.state.inputValue; + const projectId = this.validate(inputValue); + + if (projectId) { + const projectLink = document.createElement('a'); + document.body.appendChild(projectLink); + projectLink.href = `#${projectId}`; + projectLink.click(); + document.body.removeChild(projectLink); + this.handleCancel(); + } else { + console.log("Error") + } + + // window. + // this.setState({previewing: true}); + // this.props.onViewProject(); + } + handleChange (e) { + this.setState({inputValue: e.target.value}); + } + validate (input) { + //const regex1 = RegExp( + //const regex2 = RegExp('^scratch.mit.edu/projects/'); + const matches = input.match(/^(https:\/\/)?scratch\.mit\.edu\/projects\/(\d+)(\/?)$/); + if (matches != null && matches.length > 0) { + console.log("Project ID: " + matches[2]); + return matches[2]; + } + return null; + } + handleCancel () { + // this.setState({previewing: false}); + this.props.onCancel(); + } + handleGoBack () { + window.location.replace('https://scratch.mit.edu'); + } + supportedBrowser () { + if (platform.name === 'IE') { + return false; + } + return true; + } + render () { + return (this.supportedBrowser() ? + <ImportModalComponent + onCancel={this.handleCancel} + onViewProject={this.handleViewProject} + placeholder='scratch.mit.edu/projects/123456789' + onKeyPress={this.handleKeyPress} + onChange={this.handleChange} + /> : + <BrowserModalComponent + onBack={this.handleGoBack} + /> + ); + } +} + +ImportModal.propTypes = { + onViewProject: PropTypes.func, + onCancel: PropTypes.func.isRequired, +}; + +const mapStateToProps = () => ({}); + +const mapDispatchToProps = dispatch => ({ + onViewProject: () => { + dispatch(closeImportInfo()); + }, + onCancel: () => { + dispatch(closeImportInfo()); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ImportModal); diff --git a/src/containers/preview-modal.jsx b/src/containers/preview-modal.jsx index 8d50dc83c..1209f251f 100644 --- a/src/containers/preview-modal.jsx +++ b/src/containers/preview-modal.jsx @@ -8,7 +8,8 @@ import PreviewModalComponent from '../components/preview-modal/preview-modal.jsx import BrowserModalComponent from '../components/browser-modal/browser-modal.jsx'; import { - closePreviewInfo + closePreviewInfo, + openImportInfo } from '../reducers/modals'; class PreviewModal extends React.Component { @@ -16,7 +17,8 @@ class PreviewModal extends React.Component { super(props); bindAll(this, [ 'handleTryIt', - 'handleCancel' + 'handleCancel', + 'handleViewProject' ]); this.state = { @@ -30,6 +32,9 @@ class PreviewModal extends React.Component { handleCancel () { window.location.replace('https://scratch.mit.edu'); } + handleViewProject () { + this.props.onViewProject(); + } supportedBrowser () { if (platform.name === 'IE') { return false; @@ -42,6 +47,7 @@ class PreviewModal extends React.Component { previewing={this.state.previewing} onCancel={this.handleCancel} onTryIt={this.handleTryIt} + onViewProject={this.handleViewProject} /> : <BrowserModalComponent onBack={this.handleCancel} @@ -51,7 +57,8 @@ class PreviewModal extends React.Component { } PreviewModal.propTypes = { - onTryIt: PropTypes.func + onTryIt: PropTypes.func, + onViewProject: PropTypes.func }; const mapStateToProps = () => ({}); @@ -59,6 +66,10 @@ const mapStateToProps = () => ({}); const mapDispatchToProps = dispatch => ({ onTryIt: () => { dispatch(closePreviewInfo()); + }, + onViewProject: () => { + dispatch(closePreviewInfo()); + dispatch(openImportInfo()); } }); diff --git a/src/css/colors.css b/src/css/colors.css index e270217f9..5dd2f325c 100644 --- a/src/css/colors.css +++ b/src/css/colors.css @@ -19,3 +19,5 @@ $control-primary: #FFAB19; $data-primary: #FF8C1A; $form-border: #E9EEF2; + +$import-primary: #0FBD8C; diff --git a/src/reducers/modals.js b/src/reducers/modals.js index ffbca3f8c..ba038ff34 100644 --- a/src/reducers/modals.js +++ b/src/reducers/modals.js @@ -7,6 +7,7 @@ const MODAL_BACKDROP_LIBRARY = 'backdropLibrary'; const MODAL_COSTUME_LIBRARY = 'costumeLibrary'; const MODAL_EXTENSION_LIBRARY = 'extensionLibrary'; const MODAL_FEEDBACK_FORM = 'feedbackForm'; +const MODAL_IMPORT_INFO = 'importInfo'; const MODAL_PREVIEW_INFO = 'previewInfo'; const MODAL_SOUND_LIBRARY = 'soundLibrary'; const MODAL_SPRITE_LIBRARY = 'spriteLibrary'; @@ -18,6 +19,7 @@ const initialState = { [MODAL_COSTUME_LIBRARY]: false, [MODAL_EXTENSION_LIBRARY]: false, [MODAL_FEEDBACK_FORM]: false, + [MODAL_IMPORT_INFO]: false, [MODAL_PREVIEW_INFO]: true, [MODAL_SOUND_LIBRARY]: false, [MODAL_SPRITE_LIBRARY]: false, @@ -67,6 +69,14 @@ const openFeedbackForm = function () { analytics.pageview('/modals/feedback'); return openModal(MODAL_FEEDBACK_FORM); }; +const openImportInfo = function () { + analytics.pageview('modals/import'); + return openModal(MODAL_IMPORT_INFO); +}; +const openPreviewInfo = function () { + analytics.pageview('/modals/preview'); + return openModal(MODAL_PREVIEW_INFO); +}; const openSoundLibrary = function () { analytics.pageview('/libraries/sounds'); return openModal(MODAL_SOUND_LIBRARY); @@ -79,10 +89,6 @@ const openSoundRecorder = function () { analytics.pageview('/modals/microphone'); return openModal(MODAL_SOUND_RECORDER); }; -const openPreviewInfo = function () { - analytics.pageview('/modals/preview'); - return openModal(MODAL_PREVIEW_INFO); -}; const closeBackdropLibrary = function () { return closeModal(MODAL_BACKDROP_LIBRARY); }; @@ -95,6 +101,9 @@ const closeExtensionLibrary = function () { const closeFeedbackForm = function () { return closeModal(MODAL_FEEDBACK_FORM); }; +const closeImportInfo = function () { + return closeModal(MODAL_IMPORT_INFO); +}; const closePreviewInfo = function () { return closeModal(MODAL_PREVIEW_INFO); }; @@ -113,6 +122,7 @@ export { openCostumeLibrary, openExtensionLibrary, openFeedbackForm, + openImportInfo, openPreviewInfo, openSoundLibrary, openSpriteLibrary, @@ -121,6 +131,7 @@ export { closeCostumeLibrary, closeExtensionLibrary, closeFeedbackForm, + closeImportInfo, closePreviewInfo, closeSpriteLibrary, closeSoundLibrary, -- GitLab