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 || ''}"`,