feat: [UEPR-44] Implemented custom journeys for onboarding

This commit is contained in:
MiroslavDionisiev 2024-10-09 11:24:20 +03:00
parent 63cb535684
commit cb5d05f39a
26 changed files with 940 additions and 32 deletions

83
package-lock.json generated
View file

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"bunyan": "1.8.15", "bunyan": "1.8.15",
"clipboard-copy": "2.0.1", "clipboard-copy": "2.0.1",
"driver.js": "^1.3.1",
"express": "4.19.2", "express": "4.19.2",
"express-http-proxy": "1.6.3", "express-http-proxy": "1.6.3",
"lodash.defaults": "4.2.0", "lodash.defaults": "4.2.0",
@ -91,7 +92,7 @@
"postcss-loader": "4.3.0", "postcss-loader": "4.3.0",
"postcss-simple-vars": "5.0.2", "postcss-simple-vars": "5.0.2",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"query-string": "5.1.1", "query-string": "9.1.0",
"react": "16.14.0", "react": "16.14.0",
"react-dom": "16.14.0", "react-dom": "16.14.0",
"react-intl": "5.25.1", "react-intl": "5.25.1",
@ -8393,13 +8394,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/decode-uri-component": { "node_modules/decode-uri-component": {
"version": "0.2.2", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz",
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=0.10" "node": ">=14.16"
} }
}, },
"node_modules/decompress-response": { "node_modules/decompress-response": {
@ -8797,6 +8797,11 @@
"normalize-svg-path": "~0.1.0" "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": { "node_modules/dtrace-provider": {
"version": "0.8.8", "version": "0.8.8",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
@ -10566,6 +10571,18 @@
"node": ">=8" "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": { "node_modules/filtered-vector": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/filtered-vector/-/filtered-vector-1.2.5.tgz", "resolved": "https://registry.npmjs.org/filtered-vector/-/filtered-vector-1.2.5.tgz",
@ -20282,18 +20299,20 @@
} }
}, },
"node_modules/query-string": { "node_modules/query-string": {
"version": "5.1.1", "version": "9.1.0",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.1.0.tgz",
"integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", "integrity": "sha512-t6dqMECpCkqfyv2FfwVS1xcB6lgXW/0XZSaKdsCNGYkqMO76AFiJEg4vINzoDKcZa6MS7JX+OHIjwh06K5vczw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"decode-uri-component": "^0.2.0", "decode-uri-component": "^0.4.1",
"object-assign": "^4.1.0", "filter-obj": "^5.1.0",
"strict-uri-encode": "^1.0.0" "split-on-first": "^3.0.0"
}, },
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/querystringify": { "node_modules/querystringify": {
@ -22647,6 +22666,15 @@
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT" "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": { "node_modules/scratch-gui/node_modules/immutable": {
"version": "3.8.2", "version": "3.8.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
@ -22734,6 +22762,20 @@
"url": "https://opencollective.com/postcss/" "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": { "node_modules/scratch-gui/node_modules/react-intl": {
"version": "2.9.0", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.9.0.tgz", "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.9.0.tgz",
@ -23878,6 +23920,18 @@
"node": "*" "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": { "node_modules/split-polygon": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/split-polygon/-/split-polygon-1.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }

View file

@ -46,6 +46,7 @@
"dependencies": { "dependencies": {
"bunyan": "1.8.15", "bunyan": "1.8.15",
"clipboard-copy": "2.0.1", "clipboard-copy": "2.0.1",
"driver.js": "^1.3.1",
"express": "4.19.2", "express": "4.19.2",
"express-http-proxy": "1.6.3", "express-http-proxy": "1.6.3",
"lodash.defaults": "4.2.0", "lodash.defaults": "4.2.0",
@ -115,18 +116,18 @@
"lodash.merge": "4.6.2", "lodash.merge": "4.6.2",
"lodash.mergewith": "4.6.2", "lodash.mergewith": "4.6.2",
"lodash.omit": "3.1.0", "lodash.omit": "3.1.0",
"lodash.uniqby": "4.7.0",
"lodash.sample": "4.2.1", "lodash.sample": "4.2.1",
"lodash.uniqby": "4.7.0",
"mini-css-extract-plugin": "1.6.2", "mini-css-extract-plugin": "1.6.2",
"minilog": "2.1.0", "minilog": "2.1.0",
"pako": "0.2.8", "pako": "0.2.8",
"plotly.js": "1.47.4", "plotly.js": "1.47.4",
"postcss-import": "12.0.1",
"postcss": "8.4.40", "postcss": "8.4.40",
"postcss-import": "12.0.1",
"postcss-loader": "4.3.0", "postcss-loader": "4.3.0",
"postcss-simple-vars": "5.0.2", "postcss-simple-vars": "5.0.2",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"query-string": "5.1.1", "query-string": "9.1.0",
"react": "16.14.0", "react": "16.14.0",
"react-dom": "16.14.0", "react-dom": "16.14.0",
"react-intl": "5.25.1", "react-intl": "5.25.1",

View file

@ -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;

View file

@ -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}) => (
<>
<div className="title">{title}</div>
<FlexRow className="description-wrapper">
{
descriptionData.map((prop, index) => (
<FlexRow
key={index}
className="journey-option"
>
<img src={prop.imgSrc} />
<Button
className={'large'}
onClick={prop.handleOnClick}
>{prop.text}</Button>
</FlexRow>
))
}
</FlexRow>
</>
);
const EditorJourney = ({onActivateDeck, setIsOnOwnOptionPicked}) => {
const [driverObj] = useState(() => (
driver()
));
const intl = useIntl();
const steps = useMemo(
() => [{
popover: {
title: intl.formatMessage(messages.createTitle),
showButtons: ['close'],
sectionComponents: {
description: <EditorJourneyDescription
title={intl.formatMessage(messages.projectGenreTitle)}
descriptionData={[
{
imgSrc: '/images/onboarding-journeys/Games-Icon.png',
text: intl.formatMessage(messages.gameTitle),
handleOnClick: () => 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: <EditorJourneyDescription
title={intl.formatMessage(messages.typeTitle)}
descriptionData={[
{
imgSrc: '/images/onboarding-journeys/Clicker-Game.jpg',
text: intl.formatMessage(messages.clickerGameTitle),
handleOnClick: () => 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: <EditorJourneyDescription
title={intl.formatMessage(messages.typeTitle)}
descriptionData={[
{
imgSrc: '/images/onboarding-journeys/Character-Animation.jpg',
text: intl.formatMessage(messages.characterAnimationTitle),
handleOnClick: () => 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: <EditorJourneyDescription
title={intl.formatMessage(messages.typeTitle)}
descriptionData={[
{
imgSrc: '/images/onboarding-journeys/Record-Music.jpg',
text: intl.formatMessage(messages.recordSoundTitle),
handleOnClick: () => 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: <EditorJourneyDescription
title={intl.formatMessage(messages.startTitle)}
descriptionData={[
{
imgSrc: '/images/onboarding-journeys/Tutorials-Icon.png',
text: intl.formatMessage(messages.tutorialTitle),
handleOnClick: () => 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: <EditorJourneyDescription
title={intl.formatMessage(messages.startTitle)}
descriptionData={[
{
imgSrc: '/images/onboarding-journeys/Tutorials-Icon.png',
text: intl.formatMessage(messages.tutorialTitle),
handleOnClick: () => 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: <EditorJourneyDescription
title={intl.formatMessage(messages.startTitle)}
descriptionData={[
{
imgSrc: '/images/onboarding-journeys/Tutorials-Icon.png',
text: intl.formatMessage(messages.tutorialTitle),
handleOnClick: () =>
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: <EditorJourneyDescription
title={intl.formatMessage(messages.startTitle)}
descriptionData={[
{
imgSrc: '/images/onboarding-journeys/Tutorials-Icon.png',
text: intl.formatMessage(messages.tutorialTitle),
handleOnClick: () => 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: <EditorJourneyDescription
title={intl.formatMessage(messages.startTitle)}
descriptionData={[
{
imgSrc: '/images/onboarding-journeys/Tutorials-Icon.png',
text: intl.formatMessage(messages.tutorialTitle),
handleOnClick: () => 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: <EditorJourneyDescription
title={intl.formatMessage(messages.startTitle)}
descriptionData={[
{
imgSrc: '/images/onboarding-journeys/Tutorials-Icon.png',
text: intl.formatMessage(messages.tutorialTitle),
handleOnClick: () => 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 (
<DriverJourney
configProps={{
popoverClass: 'gui-journey',
overlayOpacity: 0,
steps: steps
}}
driverObj={driverObj}
/>
);
};
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;

View file

@ -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;
}
}
}

View file

@ -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 (
<DriverJourney
configProps={{
popoverClass: 'project-journey',
showButtons: [
'next',
'previous'
],
nextBtnText: 'Next',
prevBtnText: 'Previous',
showProgress: false,
steps: steps
}}
driverObj={driverObj}
/>
);
};
module.exports = ProjectJourney;

View file

@ -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;
}
}
}

View file

@ -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 (
<DriverJourney
configProps={{
popoverClass: 'tutorials-highlight',
showProgress: false,
overlayOpacity: 0,
steps: steps
}}
driverObj={driverObj}
/>
);
};
module.exports = TutorialsHighlight;

View file

@ -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;
}
}

22
src/lib/use-previous.js Normal file
View file

@ -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;
};

View file

@ -35,12 +35,14 @@ const FormsyProjectUpdater = require('./formsy-project-updater.jsx');
const EmailConfirmationModal = require('../../components/modal/email-confirmation/modal.jsx'); const EmailConfirmationModal = require('../../components/modal/email-confirmation/modal.jsx');
const EmailConfirmationBanner = require('../../components/dropdown-banner/email-confirmation/banner.jsx'); const EmailConfirmationBanner = require('../../components/dropdown-banner/email-confirmation/banner.jsx');
const {onCommented} = require('../../lib/user-guiding.js'); const {onCommented} = require('../../lib/user-guiding.js');
const queryString = require('query-string').default;
const projectShape = require('./projectshape.jsx').projectShape; const projectShape = require('./projectshape.jsx').projectShape;
require('./preview.scss'); require('./preview.scss');
const frameless = require('../../lib/frameless'); const frameless = require('../../lib/frameless');
const {useState, useCallback} = require('react'); 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 // disable enter key submission on formsy input fields; otherwise formsy thinks
// we meant to trigger the "See inside" button. Instead, treat these keypresses // we meant to trigger the "See inside" button. Instead, treat these keypresses
@ -255,6 +257,11 @@ const PreviewPresentation = ({
)} )}
{ projectInfo && projectInfo.author && projectInfo.author.id && ( { projectInfo && projectInfo.author && projectInfo.author.id && (
<React.Fragment> <React.Fragment>
{
isProjectLoaded &&
queryString.parse(location.search, {parseBooleans: true}).showJourney &&
<ProjectJourney />
}
{showEmailConfirmationBanner && <EmailConfirmationBanner {showEmailConfirmationBanner && <EmailConfirmationBanner
/* eslint-disable react/jsx-no-bind */ /* eslint-disable react/jsx-no-bind */
onRequestDismiss={() => onBannerDismiss('confirmed_email')} onRequestDismiss={() => onBannerDismiss('confirmed_email')}

View file

@ -8,7 +8,7 @@ const PropTypes = require('prop-types');
const connect = require('react-redux').connect; const connect = require('react-redux').connect;
const injectIntl = require('react-intl').injectIntl; const injectIntl = require('react-intl').injectIntl;
const parser = require('scratch-parser'); const parser = require('scratch-parser');
const queryString = require('query-string'); const queryString = require('query-string').default;
const api = require('../../lib/api'); const api = require('../../lib/api');
const Page = require('../../components/page/www/page.jsx'); 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 NotAvailable = require('../../components/not-available/not-available.jsx');
const Meta = require('./meta.jsx'); const Meta = require('./meta.jsx');
const { const {
onProjectShared, onProjectShared
onProjectLoaded
} = require('../../lib/user-guiding.js'); } = require('../../lib/user-guiding.js');
const sessionActions = require('../../redux/session.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 localStorageAvailable = 'localStorage' in window && window.localStorage !== null;
const xhr = require('xhr'); 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(() => { useEffect(() => {
if (props.projectId && props.projectId !== '0') { const isTutorialOpen = !!queryString.parse(location.search).tutorial;
onProjectLoaded(user.id, permissions);
}
}, [props.projectId, user.id, permissions]);
return <IntlGUI {...props} />; if (
props.projectId &&
prevProjectId === '0' &&
props.projectId !== '0' &&
!isTutorialOpen
) {
setShowJourney(true);
}
}, [props.projectId, prevProjectId, location]);
return (
<>
<IntlGUI {...props} />
{showJourney && (
<EditorJourney
onActivateDeck={props.onActivateDeck}
setIsOnOwnOptionPicked={setIsOnOwnOptionPicked}
/>
)}
{isOnOwnOptionPicked && <TutorialsHighlight />}
</>
);
}; };
IntlGUIWithProjectHandler.propTypes = { IntlGUIWithProjectHandler.propTypes = {
@ -691,6 +715,7 @@ class Preview extends React.Component {
const parts = window.location.pathname.toLowerCase() const parts = window.location.pathname.toLowerCase()
.split('/') .split('/')
.filter(Boolean); .filter(Boolean);
const queryParams = location.search;
let newUrl; let newUrl;
if (projectId === '0') { if (projectId === '0') {
newUrl = `/${parts[0]}/editor`; newUrl = `/${parts[0]}/editor`;
@ -702,7 +727,7 @@ class Preview extends React.Component {
history.pushState( history.pushState(
{projectId: projectId}, {projectId: projectId},
{projectId: projectId}, {projectId: projectId},
newUrl `${newUrl}${queryParams}`
); );
if (callback) callback(); if (callback) callback();
}); });
@ -907,6 +932,7 @@ class Preview extends React.Component {
onUpdateProjectTitle={this.handleUpdateProjectTitle} onUpdateProjectTitle={this.handleUpdateProjectTitle}
user={this.props.user} user={this.props.user}
permissions={this.props.permissions} permissions={this.props.permissions}
onActivateDeck={this.props.onActivateDeck}
/> />
)} )}
{this.props.registrationOpen && ( {this.props.registrationOpen && (
@ -984,6 +1010,7 @@ Preview.propTypes = {
lovedLoaded: PropTypes.bool, lovedLoaded: PropTypes.bool,
moreCommentsToLoad: PropTypes.bool, moreCommentsToLoad: PropTypes.bool,
original: projectShape, original: projectShape,
onActivateDeck: PropTypes.func,
parent: projectShape, parent: projectShape,
permissions: PropTypes.object, permissions: PropTypes.object,
playerMode: PropTypes.bool, playerMode: PropTypes.bool,
@ -1237,6 +1264,9 @@ const mapDispatchToProps = dispatch => ({
}, },
setFullScreen: fullscreen => { setFullScreen: fullscreen => {
dispatch(GUI.setFullScreen(fullscreen)); dispatch(GUI.setFullScreen(fullscreen));
},
onActivateDeck: id => {
dispatch(GUI.activateDeck(id));
} }
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -171,6 +171,7 @@ module.exports = {
loader: 'css-loader', loader: 'css-loader',
options: { options: {
modules: { modules: {
auto: true,
localIdentName: '[name]_[local]_[hash:base64:5]', localIdentName: '[name]_[local]_[hash:base64:5]',
exportLocalsConvention: 'camelCase' exportLocalsConvention: 'camelCase'
}, },
@ -271,11 +272,11 @@ module.exports = {
'process.env.ASSET_HOST': `"${process.env.ASSET_HOST || 'https://assets.scratch.mit.edu'}"`, '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.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.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.STATIC_HOST': `"${process.env.STATIC_HOST || 'https://uploads.scratch.mit.edu'}"`,
'process.env.SCRATCH_ENV': `"${process.env.SCRATCH_ENV || 'development'}"`, 'process.env.SCRATCH_ENV': `"${process.env.SCRATCH_ENV || 'development'}"`,
'process.env.THUMBNAIL_URI': `"${process.env.THUMBNAIL_URI || '/internalapi/project/thumbnail/{}/set/'}"`, 'process.env.THUMBNAIL_URI': `"${process.env.THUMBNAIL_URI || '/projects/{}/thumbnail'}"`,
'process.env.THUMBNAIL_HOST': `"${process.env.THUMBNAIL_HOST || ''}"`, 'process.env.THUMBNAIL_HOST': `"${process.env.THUMBNAIL_HOST || 'http://localhost:4001'}"`,
'process.env.DEBUG': Boolean(process.env.DEBUG), 'process.env.DEBUG': Boolean(process.env.DEBUG),
'process.env.GA_ID': `"${process.env.GA_ID || 'UA-000000-01'}"`, 'process.env.GA_ID': `"${process.env.GA_ID || 'UA-000000-01'}"`,
'process.env.GTM_ENV_AUTH': `"${process.env.GTM_ENV_AUTH || ''}"`, 'process.env.GTM_ENV_AUTH': `"${process.env.GTM_ENV_AUTH || ''}"`,