diff --git a/package-lock.json b/package-lock.json index ada194328..01bd5546e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "bunyan": "1.8.15", "clipboard-copy": "2.0.1", + "driver.js": "^1.3.1", "express": "4.19.2", "express-http-proxy": "1.6.3", "lodash.defaults": "4.2.0", @@ -91,7 +92,7 @@ "postcss-loader": "4.3.0", "postcss-simple-vars": "5.0.2", "prop-types": "15.8.1", - "query-string": "5.1.1", + "query-string": "9.1.0", "react": "16.14.0", "react-dom": "16.14.0", "react-intl": "5.25.1", @@ -8393,13 +8394,12 @@ "license": "MIT" }, "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", + "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", "dev": true, - "license": "MIT", "engines": { - "node": ">=0.10" + "node": ">=14.16" } }, "node_modules/decompress-response": { @@ -8797,6 +8797,11 @@ "normalize-svg-path": "~0.1.0" } }, + "node_modules/driver.js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.3.1.tgz", + "integrity": "sha512-MvUdXbqSgEsgS/H9KyWb5Rxy0aE6BhOVT4cssi2x2XjmXea6qQfgdx32XKVLLSqTaIw7q/uxU5Xl3NV7+cN6FQ==" + }, "node_modules/dtrace-provider": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", @@ -10566,6 +10571,18 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", + "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/filtered-vector": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/filtered-vector/-/filtered-vector-1.2.5.tgz", @@ -20282,18 +20299,20 @@ } }, "node_modules/query-string": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", - "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.1.0.tgz", + "integrity": "sha512-t6dqMECpCkqfyv2FfwVS1xcB6lgXW/0XZSaKdsCNGYkqMO76AFiJEg4vINzoDKcZa6MS7JX+OHIjwh06K5vczw==", "dev": true, - "license": "MIT", "dependencies": { - "decode-uri-component": "^0.2.0", - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" + "decode-uri-component": "^0.4.1", + "filter-obj": "^5.1.0", + "split-on-first": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/querystringify": { @@ -22647,6 +22666,15 @@ "hasInstallScript": true, "license": "MIT" }, + "node_modules/scratch-gui/node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, "node_modules/scratch-gui/node_modules/immutable": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", @@ -22734,6 +22762,20 @@ "url": "https://opencollective.com/postcss/" } }, + "node_modules/scratch-gui/node_modules/query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "dev": true, + "dependencies": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/scratch-gui/node_modules/react-intl": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.9.0.tgz", @@ -23878,6 +23920,18 @@ "node": "*" } }, + "node_modules/split-on-first": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", + "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/split-polygon": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/split-polygon/-/split-polygon-1.0.0.tgz", @@ -24260,7 +24314,6 @@ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } diff --git a/package.json b/package.json index 6768a97d6..4dea003c6 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "dependencies": { "bunyan": "1.8.15", "clipboard-copy": "2.0.1", + "driver.js": "^1.3.1", "express": "4.19.2", "express-http-proxy": "1.6.3", "lodash.defaults": "4.2.0", @@ -115,18 +116,18 @@ "lodash.merge": "4.6.2", "lodash.mergewith": "4.6.2", "lodash.omit": "3.1.0", - "lodash.uniqby": "4.7.0", "lodash.sample": "4.2.1", + "lodash.uniqby": "4.7.0", "mini-css-extract-plugin": "1.6.2", "minilog": "2.1.0", "pako": "0.2.8", "plotly.js": "1.47.4", - "postcss-import": "12.0.1", "postcss": "8.4.40", + "postcss-import": "12.0.1", "postcss-loader": "4.3.0", "postcss-simple-vars": "5.0.2", "prop-types": "15.8.1", - "query-string": "5.1.1", + "query-string": "9.1.0", "react": "16.14.0", "react-dom": "16.14.0", "react-intl": "5.25.1", diff --git a/src/components/journeys/driver-journey/driver-journey.jsx b/src/components/journeys/driver-journey/driver-journey.jsx new file mode 100644 index 000000000..bcdc86c68 --- /dev/null +++ b/src/components/journeys/driver-journey/driver-journey.jsx @@ -0,0 +1,67 @@ +const React = require('react'); +const {useState, useEffect, isValidElement} = require('react'); +const {createPortal} = require('react-dom'); +const PropTypes = require('prop-types'); +require('driver.js/dist/driver.css'); + +const DriverJourney = ({configProps, driverObj}) => { + const [renderState, setRenderState] = useState(); + + const {steps, ...restConfig} = configProps; + + useEffect(() => { + const driverSteps = steps.map((step, index) => { + const {sectionComponents = {}, ...popoverProps} = step.popover; + return { + ...step, + popover: { + ...popoverProps, + onPopoverRender: popover => { + const portalData = []; + for (const [section, component] of Object.entries( + sectionComponents + )) { + if (isValidElement(component)) { + popover[section].style.display = 'block'; + popover[section].innerHTML = ''; + portalData.push({ + parentElement: popover[section], + childElement: component + }); + } + } + + setRenderState({components: portalData, stepIndex: index}); + } + } + }; + }); + + driverObj.setConfig({...restConfig, steps: driverSteps}); + + driverObj.drive(); + }, [driverObj, steps]); + + if (!renderState) return null; + if (!steps[renderState.stepIndex]) return null; + + return ( + <> + {renderState.components.map(obj => + createPortal(obj.childElement, obj.parentElement) + )} + + ); +}; + +DriverJourney.propTypes = { + configProps: PropTypes.shape({ + steps: PropTypes.arrayOf(PropTypes.object) + }), + driverObj: PropTypes.shape({ + setConfig: PropTypes.func, + drive: PropTypes.func + }) +}; + +module.exports = DriverJourney; diff --git a/src/components/journeys/editor-journey/editor-journey.jsx b/src/components/journeys/editor-journey/editor-journey.jsx new file mode 100644 index 000000000..ebdc99f89 --- /dev/null +++ b/src/components/journeys/editor-journey/editor-journey.jsx @@ -0,0 +1,469 @@ +const React = require('react'); +const {driver} = require('driver.js'); +const FlexRow = require('../../flex-row/flex-row.jsx'); +const Button = require('../../forms/button.jsx'); +const DriverJourney = require('../driver-journey/driver-journey.jsx'); +const {defineMessages, useIntl} = require('react-intl'); +const {useMemo, useState} = require('react'); +const PropTypes = require('prop-types'); + +require('./editor-journey.scss'); + +const messages = defineMessages({ + createTitle: { + id: 'gui.journey.controls.create', + defaultMessage: 'Create', + description: 'Create modal title' + }, + projectGenreTitle: { + id: 'gui.journey.controls.choose.projectGenre', + defaultMessage: 'What do you whant to create?', + description: 'Choose project genre modal title' + }, + typeTitle: { + id: 'gui.journey.controls.choose.type', + defaultMessage: 'Which type?', + description: 'Choose project type modal title' + }, + startTitle: { + id: 'gui.journey.controls.choose.start', + defaultMessage: 'How do you want to start?', + description: 'Choose way to start modal title' + }, + gameTitle: { + id: 'gui.journey.controls.game', + defaultMessage: 'Game', + description: 'Game button title' + }, + animiationTitle: { + id: 'gui.journey.controls.animation', + defaultMessage: 'Animation', + description: 'Animation button title' + }, + musicTitle: { + id: 'gui.journey.controls.music', + defaultMessage: 'Music', + description: 'Music button title' + }, + clickerGameTitle: { + id: 'gui.journey.controls.game.clicker', + defaultMessage: 'Clicker Game', + description: 'Clicker game button title' + }, + pongGameTitle: { + id: 'gui.journey.controls.game.pong', + defaultMessage: 'Pong Game', + description: 'Pong game button title' + }, + characterAnimationTitle: { + id: 'gui.journey.controls.animation.character', + defaultMessage: 'Animate a character', + description: 'Animate a character button title' + }, + flyAnimationTitle: { + id: 'gui.journey.controls.animation.fly', + defaultMessage: 'Make it fly', + description: 'Make it fly animation button title' + }, + recordSoundTitle: { + id: 'gui.journey.controls.music.record', + defaultMessage: 'Record a sound', + description: 'Record a sound button title' + }, + makeMusicTitle: { + id: 'gui.journey.controls.music.make', + defaultMessage: 'Make music', + description: 'Make music button title' + }, + tutorialTitle: { + id: 'gui.journey.controls.tutorial', + defaultMessage: 'Tutorial', + description: 'Tutorial button title' + }, + starterProjectTitle: { + id: 'gui.journey.controls.starterProject', + defaultMessage: 'Starter project', + description: 'Starter project button title' + }, + onMyOwnTitle: { + id: 'gui.journey.controls.onMyOwn', + defaultMessage: 'On my own', + description: 'On my own button title' + } +}); + +const projects = { + clicker: '10128368', + pong: '10128515', + animateCharacter: '10128067', + makeItFly: '114019829', + recordSound: '1031325137', + makeMusic: '10012676' +}; + +const tutorialIds = { + clicker: { + id: 'Make-A-Game', + urlId: 'clicker-game' + }, + pong: { + id: 'pong', + urlId: 'pong' + }, + animateCharacter: { + id: 'Animate-A-Character', + urlId: 'animate-a-character' + }, + makeItFly: { + id: 'make-it-fly', + urlId: 'make-it-fly' + }, + recordSound: { + id: 'record-a-sound', + urlId: 'record-a-sound' + }, + makeMusic: { + id: 'Make-Music', + urlId: 'music' + } +}; + +const redirectToProject = projectId => { + location.href = `/projects/${projectId}?showJourney=true`; +}; + +const openTutorial = (onActivateDeck, tutorial, driverObj) => { + history.pushState({}, {}, `?tutorial=${tutorial.urlId}`); + onActivateDeck(tutorial.id); + driverObj.destroy(); +}; + +const ownOptingPicked = (setIsOnOwnOptionPicked, driverObg) => { + setIsOnOwnOptionPicked(true); + driverObg.destroy(); +}; + +const EditorJourneyDescription = ({title, descriptionData}) => ( + <> +
{title}
+ + { + descriptionData.map((prop, index) => ( + + + + + )) + } + + +); + +const EditorJourney = ({onActivateDeck, setIsOnOwnOptionPicked}) => { + const [driverObj] = useState(() => ( + driver() + )); + const intl = useIntl(); + + const steps = useMemo( + () => [{ + popover: { + title: intl.formatMessage(messages.createTitle), + showButtons: ['close'], + sectionComponents: { + description: driverObj.moveTo(1) + }, + { + imgSrc: '/images/onboarding-journeys/Animation-Icon.png', + text: intl.formatMessage(messages.animiationTitle), + handleOnClick: () => driverObj.moveTo(2) + }, + { + imgSrc: '/images/onboarding-journeys/Music-Icon.png', + text: intl.formatMessage(messages.musicTitle), + handleOnClick: () => driverObj.moveTo(3) + } + ]} + /> + } + } + }, + { + popover: { + title: intl.formatMessage(messages.createTitle), + showButtons: ['close'], + sectionComponents: { + description: driverObj.moveTo(4) + }, + { + imgSrc: '/images/onboarding-journeys/Pong-Game.jpg', + text: intl.formatMessage(messages.pongGameTitle), + handleOnClick: () => driverObj.moveTo(5) + } + ]} + /> + } + } + }, + { + popover: { + title: intl.formatMessage(messages.createTitle), + showButtons: ['close'], + sectionComponents: { + description: driverObj.moveTo(6) + }, + { + imgSrc: '/images/onboarding-journeys/Fly-Animation.jpg', + text: intl.formatMessage(messages.flyAnimationTitle), + handleOnClick: () => driverObj.moveTo(7) + } + ]} + /> + } + } + }, + { + popover: { + title: intl.formatMessage(messages.createTitle), + showButtons: ['close'], + sectionComponents: { + description: driverObj.moveTo(8) + }, + { + imgSrc: '/images/onboarding-journeys/Make-Music.jpg', + text: intl.formatMessage(messages.makeMusicTitle), + handleOnClick: () => driverObj.moveTo(9) + } + ]} + /> + } + } + }, + { + popover: { + title: intl.formatMessage(messages.createTitle), + showButtons: ['close'], + sectionComponents: { + description: openTutorial(onActivateDeck, tutorialIds.clicker, driverObj) + }, + { + imgSrc: '/images/onboarding-journeys/Starter-Projects-Icon.png', + text: intl.formatMessage(messages.starterProjectTitle), + handleOnClick: () => redirectToProject(projects.clicker) + }, + { + imgSrc: '/images/onboarding-journeys/On-Own-Icon.png', + text: intl.formatMessage(messages.onMyOwnTitle), + handleOnClick: () => ownOptingPicked(setIsOnOwnOptionPicked, driverObj) + } + ]} + /> + } + } + }, + { + popover: { + title: intl.formatMessage(messages.createTitle), + showButtons: ['close'], + sectionComponents: { + description: openTutorial(onActivateDeck, tutorialIds.pong, driverObj) + }, + { + imgSrc: '/images/onboarding-journeys/Starter-Projects-Icon.png', + text: intl.formatMessage(messages.starterProjectTitle), + handleOnClick: () => redirectToProject(projects.pong) + }, + { + imgSrc: '/images/onboarding-journeys/On-Own-Icon.png', + text: intl.formatMessage(messages.onMyOwnTitle), + handleOnClick: () => ownOptingPicked(setIsOnOwnOptionPicked, driverObj) + } + ]} + /> + } + } + }, + { + popover: { + title: intl.formatMessage(messages.createTitle), + showButtons: ['close'], + sectionComponents: { + description: + openTutorial(onActivateDeck, tutorialIds.animateCharacter, driverObj) + }, + { + imgSrc: '/images/onboarding-journeys/Starter-Projects-Icon.png', + text: intl.formatMessage(messages.starterProjectTitle), + handleOnClick: () => redirectToProject(projects.animateCharacter) + }, + { + imgSrc: '/images/onboarding-journeys/On-Own-Icon.png', + text: intl.formatMessage(messages.onMyOwnTitle), + handleOnClick: () => ownOptingPicked(setIsOnOwnOptionPicked, driverObj) + } + ]} + /> + } + } + }, + { + popover: { + title: intl.formatMessage(messages.createTitle), + showButtons: ['close'], + sectionComponents: { + description: openTutorial(onActivateDeck, tutorialIds.makeItFly, driverObj) + }, + { + imgSrc: '/images/onboarding-journeys/Starter-Projects-Icon.png', + text: intl.formatMessage(messages.starterProjectTitle), + handleOnClick: () => redirectToProject(projects.makeItFly) + }, + { + imgSrc: '/images/onboarding-journeys/On-Own-Icon.png', + text: intl.formatMessage(messages.onMyOwnTitle), + handleOnClick: () => ownOptingPicked(setIsOnOwnOptionPicked, driverObj) + } + ]} + /> + } + } + }, + { + popover: { + title: intl.formatMessage(messages.createTitle), + showButtons: ['close'], + sectionComponents: { + description: openTutorial(onActivateDeck, tutorialIds.recordSound, driverObj) + }, + { + imgSrc: '/images/onboarding-journeys/Starter-Projects-Icon.png', + text: intl.formatMessage(messages.starterProjectTitle), + handleOnClick: () => redirectToProject(projects.recordSound) + }, + { + imgSrc: '/images/onboarding-journeys/On-Own-Icon.png', + text: intl.formatMessage(messages.onMyOwnTitle), + handleOnClick: () => ownOptingPicked(setIsOnOwnOptionPicked, driverObj) + } + ]} + /> + } + } + }, + { + popover: { + title: intl.formatMessage(messages.createTitle), + showButtons: ['close'], + sectionComponents: { + description: openTutorial(onActivateDeck, tutorialIds.makeMusic, driverObj) + }, + { + imgSrc: '/images/onboarding-journeys/Starter-Projects-Icon.png', + text: intl.formatMessage(messages.starterProjectTitle), + handleOnClick: () => redirectToProject(projects.makeMusic) + }, + { + imgSrc: '/images/onboarding-journeys/On-Own-Icon.png', + text: intl.formatMessage(messages.onMyOwnTitle), + handleOnClick: () => ownOptingPicked(setIsOnOwnOptionPicked, driverObj) + } + ]} + /> + } + } + }], [onActivateDeck, setIsOnOwnOptionPicked] + ); + + return ( + + ); +}; + +EditorJourneyDescription.propTypes = { + title: PropTypes.string, + descriptionData: PropTypes.arrayOf(PropTypes.shape({ + imgSrc: PropTypes.string, + text: PropTypes.string, + handleOnClick: PropTypes.func + })) +}; + +EditorJourney.propTypes = { + onActivateDeck: PropTypes.func, + setIsOnOwnOptionPicked: PropTypes.func +}; + +module.exports = EditorJourney; diff --git a/src/components/journeys/editor-journey/editor-journey.scss b/src/components/journeys/editor-journey/editor-journey.scss new file mode 100644 index 000000000..32bdcef9b --- /dev/null +++ b/src/components/journeys/editor-journey/editor-journey.scss @@ -0,0 +1,65 @@ +@import "../../../colors"; +@import "../../../frameless"; + +.driver-popover.gui-journey { + font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif; + max-width: unset; + padding: 0; + border-radius: 15px; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%); + + .driver-popover-close-btn { + height: 2.5rem; + width: 2.5rem; + border-radius: 50%; + margin: 0.5rem; + font-size: 2rem; + font-weight: bold; + color: $type-white; + background-color: $ui-aqua-dark; + } + + .driver-popover-title { + padding: 1rem 0; + font-size: 1rem; + font-weight: 700; + text-align: center; + color: $type-white; + margin: 0; + background-color: $ui-aqua; + border-radius: 15px 15px 0 0; + } + + .driver-popover-title[style*=block]+.driver-popover-description { + margin: 0; + } +} + +.title { + padding: 1rem 0; + font-size: 1.125rem; + font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif; + font-weight: 400; + text-align: center; + color: $type-gray; + background-color: $ui-light-primary; +} + +.description-wrapper { + flex-direction: row; + justify-content: space-evenly; + gap: 2rem; + margin: 3rem 4rem; + + .journey-option { + flex-direction: column; + justify-content: center; + gap: 1rem; + + img { + max-height: $cols2; + } + } +} diff --git a/src/components/journeys/project-journey/project-journey.jsx b/src/components/journeys/project-journey/project-journey.jsx new file mode 100644 index 000000000..8c2b45f3a --- /dev/null +++ b/src/components/journeys/project-journey/project-journey.jsx @@ -0,0 +1,58 @@ +const React = require('react'); +const {driver} = require('driver.js'); +const DriverJourney = require('../driver-journey/driver-journey.jsx'); +const {defineMessages, useIntl} = require('react-intl'); +require('./project-journey.scss'); + +const messages = defineMessages({ + playProject: { + id: 'project.journey.play', + defaultMessage: 'Click green flag to play', + description: 'Play project' + }, + remixProject: { + id: 'project.journey.remix', + defaultMessage: 'Make your own version!', + description: 'Remix project' + } +}); + +const ProjectJourney = () => { + const [driverObj] = React.useState(() => ( + driver() + )); + + const intl = useIntl(); + + const steps = [{ + element: 'div[class^="stage_green-flag-overlay-wrapper"] > div', + popover: { + description: intl.formatMessage(messages.playProject) + } + }, + { + element: '.remix-button', + popover: { + description: intl.formatMessage(messages.remixProject) + } + }]; + + return ( + + ); +}; + +module.exports = ProjectJourney; diff --git a/src/components/journeys/project-journey/project-journey.scss b/src/components/journeys/project-journey/project-journey.scss new file mode 100644 index 000000000..4c67ca5b1 --- /dev/null +++ b/src/components/journeys/project-journey/project-journey.scss @@ -0,0 +1,50 @@ +@import "../../../colors"; + +.driver-popover.project-journey { + background-color: $ui-purple-dark; + + .driver-popover-arrow-side-left.driver-popover-arrow { + border-left-color: $ui-purple-dark;; + } + + .driver-popover-arrow-side-right.driver-popover-arrow { + border-right-color: $ui-purple-dark;; + } + + .driver-popover-arrow-side-top.driver-popover-arrow { + border-top-color: $ui-purple-dark;; + } + + .driver-popover-arrow-side-bottom.driver-popover-arrow { + border-bottom-color: $ui-purple-dark;; + } + + .driver-popover-description { + color: $ui-white; + font-size: 1rem; + font-weight: 700; + font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif; + } + + .driver-popover-navigation-btns { + display: flex; + justify-content: space-evenly; + + .driver-popover-btn-disabled { + opacity: 1; + } + + button { + display: inline-block; + border: 0; + border-radius: 2rem; + cursor: pointer; + padding: 0.75rem 1rem; + font-size: 0.8rem; + font-weight: bold; + + background-color: $ui-white; + color: $ui-purple-dark; + } + } +} \ No newline at end of file diff --git a/src/components/journeys/tutorials-highlight/tutorials-highlight.jsx b/src/components/journeys/tutorials-highlight/tutorials-highlight.jsx new file mode 100644 index 000000000..a49b7c2ad --- /dev/null +++ b/src/components/journeys/tutorials-highlight/tutorials-highlight.jsx @@ -0,0 +1,44 @@ +const React = require('react'); +const {driver} = require('driver.js'); +const DriverJourney = require('../driver-journey/driver-journey.jsx'); +const {defineMessages, useIntl} = require('react-intl'); +require('./tutorials-highlight.scss'); + +const messages = defineMessages({ + tutorialsHighlight: { + id: 'gui.highlight.tutorials', + defaultMessage: 'Click here for tutorials', + description: 'Tutorials highlight' + } +}); + +const TutorialsHighlight = () => { + const [driverObj] = React.useState(() => ( + driver() + )); + + const intl = useIntl(); + + const steps = [{ + element: '.tutorials-button', + popover: { + showButtons: ['close'], + side: 'bottom', + description: intl.formatMessage(messages.tutorialsHighlight) + } + }]; + + return ( + + ); +}; + +module.exports = TutorialsHighlight; diff --git a/src/components/journeys/tutorials-highlight/tutorials-highlight.scss b/src/components/journeys/tutorials-highlight/tutorials-highlight.scss new file mode 100644 index 000000000..b329b03f0 --- /dev/null +++ b/src/components/journeys/tutorials-highlight/tutorials-highlight.scss @@ -0,0 +1,41 @@ +@import "../../../colors"; + +.driver-popover.tutorials-highlight { + display: flex; + flex-direction: column; + background-color: $ui-purple-dark; + + .driver-popover-close-btn { + height: 2.5rem; + width: 2.5rem; + border-radius: 50%; + margin: 0.5rem; + font-size: 2rem; + font-weight: bold; + color: $type-white; + background-color: $ui-purple-dark; + } + + .driver-popover-arrow-side-left.driver-popover-arrow { + border-left-color: $ui-purple-dark;; + } + + .driver-popover-arrow-side-right.driver-popover-arrow { + border-right-color: $ui-purple-dark;; + } + + .driver-popover-arrow-side-top.driver-popover-arrow { + border-top-color: $ui-purple-dark;; + } + + .driver-popover-arrow-side-bottom.driver-popover-arrow { + border-bottom-color: $ui-purple-dark;; + } + + .driver-popover-description { + color: $ui-white; + font-size: 1rem; + font-weight: 700; + font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif; + } +} \ No newline at end of file diff --git a/src/lib/use-previous.js b/src/lib/use-previous.js new file mode 100644 index 000000000..b266132db --- /dev/null +++ b/src/lib/use-previous.js @@ -0,0 +1,22 @@ +import {useState} from 'react'; + +export const usePrevious = ( + value, + comparator = (prev, current) => prev === current +) => { + const [state, setState] = useState({ + value: value, + prev: null + }); + + const current = state.value; + + if (!comparator(current, value)) { + setState({ + value: value, + prev: current + }); + } + + return state.prev; +}; diff --git a/src/views/preview/presentation.jsx b/src/views/preview/presentation.jsx index 6b8e80c03..d601374cb 100644 --- a/src/views/preview/presentation.jsx +++ b/src/views/preview/presentation.jsx @@ -35,12 +35,14 @@ const FormsyProjectUpdater = require('./formsy-project-updater.jsx'); const EmailConfirmationModal = require('../../components/modal/email-confirmation/modal.jsx'); const EmailConfirmationBanner = require('../../components/dropdown-banner/email-confirmation/banner.jsx'); const {onCommented} = require('../../lib/user-guiding.js'); +const queryString = require('query-string').default; const projectShape = require('./projectshape.jsx').projectShape; require('./preview.scss'); const frameless = require('../../lib/frameless'); const {useState, useCallback} = require('react'); +const ProjectJourney = require('../../components/journeys/project-journey/project-journey.jsx'); // disable enter key submission on formsy input fields; otherwise formsy thinks // we meant to trigger the "See inside" button. Instead, treat these keypresses @@ -255,6 +257,11 @@ const PreviewPresentation = ({ )} { projectInfo && projectInfo.author && projectInfo.author.id && ( + { + isProjectLoaded && + queryString.parse(location.search, {parseBooleans: true}).showJourney && + + } {showEmailConfirmationBanner && onBannerDismiss('confirmed_email')} diff --git a/src/views/preview/project-view.jsx b/src/views/preview/project-view.jsx index b42fd67d0..8b6696afc 100644 --- a/src/views/preview/project-view.jsx +++ b/src/views/preview/project-view.jsx @@ -8,7 +8,7 @@ const PropTypes = require('prop-types'); const connect = require('react-redux').connect; const injectIntl = require('react-intl').injectIntl; const parser = require('scratch-parser'); -const queryString = require('query-string'); +const queryString = require('query-string').default; const api = require('../../lib/api'); const Page = require('../../components/page/www/page.jsx'); @@ -26,8 +26,7 @@ const CanceledDeletionModal = require('../../components/login/canceled-deletion- const NotAvailable = require('../../components/not-available/not-available.jsx'); const Meta = require('./meta.jsx'); const { - onProjectShared, - onProjectLoaded + onProjectShared } = require('../../lib/user-guiding.js'); const sessionActions = require('../../redux/session.js'); @@ -44,16 +43,41 @@ const IntlGUI = injectIntl(GUI.default); const localStorageAvailable = 'localStorage' in window && window.localStorage !== null; const xhr = require('xhr'); -const {useEffect} = require('react'); +const {useEffect, useState} = require('react'); +const EditorJourney = require('../../components/journeys/editor-journey/editor-journey.jsx'); +const {usePrevious} = require('react-use'); +const TutorialsHighlight = require('../../components/journeys/tutorials-highlight/tutorials-highlight.jsx'); + +const IntlGUIWithProjectHandler = ({...props}) => { + const [showJourney, setShowJourney] = useState(false); + const [isOnOwnOptionPicked, setIsOnOwnOptionPicked] = useState(false); + const prevProjectId = usePrevious(props.projectId); -const IntlGUIWithProjectHandler = ({user, permissions, ...props}) => { useEffect(() => { - if (props.projectId && props.projectId !== '0') { - onProjectLoaded(user.id, permissions); - } - }, [props.projectId, user.id, permissions]); + const isTutorialOpen = !!queryString.parse(location.search).tutorial; - return ; + if ( + props.projectId && + prevProjectId === '0' && + props.projectId !== '0' && + !isTutorialOpen + ) { + setShowJourney(true); + } + }, [props.projectId, prevProjectId, location]); + + return ( + <> + + {showJourney && ( + + )} + {isOnOwnOptionPicked && } + + ); }; IntlGUIWithProjectHandler.propTypes = { @@ -691,6 +715,7 @@ class Preview extends React.Component { const parts = window.location.pathname.toLowerCase() .split('/') .filter(Boolean); + const queryParams = location.search; let newUrl; if (projectId === '0') { newUrl = `/${parts[0]}/editor`; @@ -702,7 +727,7 @@ class Preview extends React.Component { history.pushState( {projectId: projectId}, {projectId: projectId}, - newUrl + `${newUrl}${queryParams}` ); if (callback) callback(); }); @@ -907,6 +932,7 @@ class Preview extends React.Component { onUpdateProjectTitle={this.handleUpdateProjectTitle} user={this.props.user} permissions={this.props.permissions} + onActivateDeck={this.props.onActivateDeck} /> )} {this.props.registrationOpen && ( @@ -984,6 +1010,7 @@ Preview.propTypes = { lovedLoaded: PropTypes.bool, moreCommentsToLoad: PropTypes.bool, original: projectShape, + onActivateDeck: PropTypes.func, parent: projectShape, permissions: PropTypes.object, playerMode: PropTypes.bool, @@ -1237,6 +1264,9 @@ const mapDispatchToProps = dispatch => ({ }, setFullScreen: fullscreen => { dispatch(GUI.setFullScreen(fullscreen)); + }, + onActivateDeck: id => { + dispatch(GUI.activateDeck(id)); } }); diff --git a/static/images/onboarding-journeys/Animation-Icon.png b/static/images/onboarding-journeys/Animation-Icon.png new file mode 100644 index 000000000..1d13a85c0 Binary files /dev/null and b/static/images/onboarding-journeys/Animation-Icon.png differ diff --git a/static/images/onboarding-journeys/Character-Animation.jpg b/static/images/onboarding-journeys/Character-Animation.jpg new file mode 100644 index 000000000..1fddfb5c5 Binary files /dev/null and b/static/images/onboarding-journeys/Character-Animation.jpg differ diff --git a/static/images/onboarding-journeys/Clicker-Game.jpg b/static/images/onboarding-journeys/Clicker-Game.jpg new file mode 100644 index 000000000..dfe78f637 Binary files /dev/null and b/static/images/onboarding-journeys/Clicker-Game.jpg differ diff --git a/static/images/onboarding-journeys/Fly-Animation.jpg b/static/images/onboarding-journeys/Fly-Animation.jpg new file mode 100644 index 000000000..3bce97750 Binary files /dev/null and b/static/images/onboarding-journeys/Fly-Animation.jpg differ diff --git a/static/images/onboarding-journeys/Games-Icon.png b/static/images/onboarding-journeys/Games-Icon.png new file mode 100644 index 000000000..071762baa Binary files /dev/null and b/static/images/onboarding-journeys/Games-Icon.png differ diff --git a/static/images/onboarding-journeys/Make-Music.jpg b/static/images/onboarding-journeys/Make-Music.jpg new file mode 100644 index 000000000..a00248320 Binary files /dev/null and b/static/images/onboarding-journeys/Make-Music.jpg differ diff --git a/static/images/onboarding-journeys/Music-Icon.png b/static/images/onboarding-journeys/Music-Icon.png new file mode 100644 index 000000000..87e8a6ba9 Binary files /dev/null and b/static/images/onboarding-journeys/Music-Icon.png differ diff --git a/static/images/onboarding-journeys/Name-Art.jpg b/static/images/onboarding-journeys/Name-Art.jpg new file mode 100644 index 000000000..391eb8971 Binary files /dev/null and b/static/images/onboarding-journeys/Name-Art.jpg differ diff --git a/static/images/onboarding-journeys/On-Own-Icon.png b/static/images/onboarding-journeys/On-Own-Icon.png new file mode 100644 index 000000000..e9468d46e Binary files /dev/null and b/static/images/onboarding-journeys/On-Own-Icon.png differ diff --git a/static/images/onboarding-journeys/Pong-Game.jpg b/static/images/onboarding-journeys/Pong-Game.jpg new file mode 100644 index 000000000..631aa1521 Binary files /dev/null and b/static/images/onboarding-journeys/Pong-Game.jpg differ diff --git a/static/images/onboarding-journeys/Record-Music.jpg b/static/images/onboarding-journeys/Record-Music.jpg new file mode 100644 index 000000000..355ff325b Binary files /dev/null and b/static/images/onboarding-journeys/Record-Music.jpg differ diff --git a/static/images/onboarding-journeys/Starter-Projects-Icon.png b/static/images/onboarding-journeys/Starter-Projects-Icon.png new file mode 100644 index 000000000..4e752deec Binary files /dev/null and b/static/images/onboarding-journeys/Starter-Projects-Icon.png differ diff --git a/static/images/onboarding-journeys/Tutorials-Icon.png b/static/images/onboarding-journeys/Tutorials-Icon.png new file mode 100644 index 000000000..0855c5665 Binary files /dev/null and b/static/images/onboarding-journeys/Tutorials-Icon.png differ diff --git a/webpack.config.js b/webpack.config.js index 8f71b2b00..304e7aae1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -171,6 +171,7 @@ module.exports = { loader: 'css-loader', options: { modules: { + auto: true, localIdentName: '[name]_[local]_[hash:base64:5]', exportLocalsConvention: 'camelCase' }, @@ -271,11 +272,11 @@ module.exports = { 'process.env.ASSET_HOST': `"${process.env.ASSET_HOST || 'https://assets.scratch.mit.edu'}"`, 'process.env.BACKPACK_HOST': `"${process.env.BACKPACK_HOST || 'https://backpack.scratch.mit.edu'}"`, 'process.env.CLOUDDATA_HOST': `"${process.env.CLOUDDATA_HOST || 'clouddata.scratch.mit.edu'}"`, - 'process.env.PROJECT_HOST': `"${process.env.PROJECT_HOST || 'https://projects.scratch.mit.edu'}"`, + 'process.env.PROJECT_HOST': `"${process.env.PROJECT_HOST || 'http://localhost:8444'}"`, 'process.env.STATIC_HOST': `"${process.env.STATIC_HOST || 'https://uploads.scratch.mit.edu'}"`, 'process.env.SCRATCH_ENV': `"${process.env.SCRATCH_ENV || 'development'}"`, - 'process.env.THUMBNAIL_URI': `"${process.env.THUMBNAIL_URI || '/internalapi/project/thumbnail/{}/set/'}"`, - 'process.env.THUMBNAIL_HOST': `"${process.env.THUMBNAIL_HOST || ''}"`, + 'process.env.THUMBNAIL_URI': `"${process.env.THUMBNAIL_URI || '/projects/{}/thumbnail'}"`, + 'process.env.THUMBNAIL_HOST': `"${process.env.THUMBNAIL_HOST || 'http://localhost:4001'}"`, 'process.env.DEBUG': Boolean(process.env.DEBUG), 'process.env.GA_ID': `"${process.env.GA_ID || 'UA-000000-01'}"`, 'process.env.GTM_ENV_AUTH': `"${process.env.GTM_ENV_AUTH || ''}"`,