about summary refs log tree commit diff
path: root/app/javascript/mastodon/features/introduction
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2018-12-17 11:07:17 +0100
committerGitHub <noreply@github.com>2018-12-17 11:07:17 +0100
commit9cb26bb56b6b61e4e8577519347ada40a7751cd6 (patch)
tree5a95798d4ef4b17df6798539c6a44ccebf9284a1 /app/javascript/mastodon/features/introduction
parentbfd0ebf92593d048d16a3882ddf44f83fa28cee2 (diff)
Add new first-time tutorial (#9531)
* Prepare to load onboarding as a full page

* Update the first-time introduction

* Improve responsive design

* Replace speech bubble with logo

* Increase text size and reword first paragraph
Diffstat (limited to 'app/javascript/mastodon/features/introduction')
-rw-r--r--app/javascript/mastodon/features/introduction/index.js196
1 files changed, 196 insertions, 0 deletions
diff --git a/app/javascript/mastodon/features/introduction/index.js b/app/javascript/mastodon/features/introduction/index.js
new file mode 100644
index 000000000..6e0617f72
--- /dev/null
+++ b/app/javascript/mastodon/features/introduction/index.js
@@ -0,0 +1,196 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ReactSwipeableViews from 'react-swipeable-views';
+import classNames from 'classnames';
+import { connect } from 'react-redux';
+import { FormattedMessage } from 'react-intl';
+import { closeOnboarding } from '../../actions/onboarding';
+import screenHello from '../../../images/screen_hello.svg';
+import screenFederation from '../../../images/screen_federation.svg';
+import screenInteractions from '../../../images/screen_interactions.svg';
+import logoTransparent from '../../../images/logo_transparent.svg';
+
+const FrameWelcome = ({ domain, onNext }) => (
+  <div className='introduction__frame'>
+    <div className='introduction__illustration' style={{ background: `url(${logoTransparent}) no-repeat center center / auto 80%` }}>
+      <img src={screenHello} alt='' />
+    </div>
+
+    <div className='introduction__text introduction__text--centered'>
+      <h3><FormattedMessage id='introduction.welcome.headline' defaultMessage='First steps' /></h3>
+      <p><FormattedMessage id='introduction.welcome.text' defaultMessage="Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name." values={{ domain: <code>{domain}</code> }} /></p>
+    </div>
+
+    <div className='introduction__action'>
+      <button className='button' onClick={onNext}><FormattedMessage id='introduction.welcome.action' defaultMessage="Let's go!" /></button>
+    </div>
+  </div>
+);
+
+FrameWelcome.propTypes = {
+  domain: PropTypes.string.isRequired,
+  onNext: PropTypes.func.isRequired,
+};
+
+const FrameFederation = ({ onNext }) => (
+  <div className='introduction__frame'>
+    <div className='introduction__illustration'>
+      <img src={screenFederation} alt='' />
+    </div>
+
+    <div className='introduction__text introduction__text--columnized'>
+      <div>
+        <h3><FormattedMessage id='introduction.federation.home.headline' defaultMessage='Home' /></h3>
+        <p><FormattedMessage id='introduction.federation.home.text' defaultMessage='Posts from people you follow will appear in your home feed. You can follow anyone on any server!' /></p>
+      </div>
+
+      <div>
+        <h3><FormattedMessage id='introduction.federation.local.headline' defaultMessage='Local' /></h3>
+        <p><FormattedMessage id='introduction.federation.local.text' defaultMessage='Public posts from people on the same server as you will appear in the local timeline.' /></p>
+      </div>
+
+      <div>
+        <h3><FormattedMessage id='introduction.federation.federated.headline' defaultMessage='Federated' /></h3>
+        <p><FormattedMessage id='introduction.federation.federated.text' defaultMessage='Public posts from other servers of the fediverse will appear in the federated timeline.' /></p>
+      </div>
+    </div>
+
+    <div className='introduction__action'>
+      <button className='button' onClick={onNext}><FormattedMessage id='introduction.federation.action' defaultMessage='Next' /></button>
+    </div>
+  </div>
+);
+
+FrameFederation.propTypes = {
+  onNext: PropTypes.func.isRequired,
+};
+
+const FrameInteractions = ({ onNext }) => (
+  <div className='introduction__frame'>
+    <div className='introduction__illustration'>
+      <img src={screenInteractions} alt='' />
+    </div>
+
+    <div className='introduction__text introduction__text--columnized'>
+      <div>
+        <h3><FormattedMessage id='introduction.interactions.reply.headline' defaultMessage='Reply' /></h3>
+        <p><FormattedMessage id='introduction.interactions.reply.text' defaultMessage="You can reply to other people's and your own toots, which will chain them together in a conversation." /></p>
+      </div>
+
+      <div>
+        <h3><FormattedMessage id='introduction.interactions.reblog.headline' defaultMessage='Boost' /></h3>
+        <p><FormattedMessage id='introduction.interactions.reblog.text' defaultMessage="You can share other people's toots with your followers by boosting them." /></p>
+      </div>
+
+      <div>
+        <h3><FormattedMessage id='introduction.interactions.favourite.headline' defaultMessage='Favourite' /></h3>
+        <p><FormattedMessage id='introduction.interactions.favourite.text' defaultMessage='You can save a toot for later, and let the author know that you liked it, by favouriting it.' /></p>
+      </div>
+    </div>
+
+    <div className='introduction__action'>
+      <button className='button' onClick={onNext}><FormattedMessage id='introduction.interactions.action' defaultMessage='Finish tutorial!' /></button>
+    </div>
+  </div>
+);
+
+FrameInteractions.propTypes = {
+  onNext: PropTypes.func.isRequired,
+};
+
+@connect(state => ({ domain: state.getIn(['meta', 'domain']) }))
+export default class Introduction extends React.PureComponent {
+
+  static propTypes = {
+    domain: PropTypes.string.isRequired,
+    dispatch: PropTypes.func.isRequired,
+  };
+
+  state = {
+    currentIndex: 0,
+  };
+
+  componentWillMount () {
+    this.pages = [
+      <FrameWelcome domain={this.props.domain} onNext={this.handleNext} />,
+      <FrameFederation onNext={this.handleNext} />,
+      <FrameInteractions onNext={this.handleFinish} />,
+    ];
+  }
+
+  componentDidMount() {
+    window.addEventListener('keyup', this.handleKeyUp);
+  }
+
+  componentWillUnmount() {
+    window.addEventListener('keyup', this.handleKeyUp);
+  }
+
+  handleDot = (e) => {
+    const i = Number(e.currentTarget.getAttribute('data-index'));
+    e.preventDefault();
+    this.setState({ currentIndex: i });
+  }
+
+  handlePrev = () => {
+    this.setState(({ currentIndex }) => ({
+      currentIndex: Math.max(0, currentIndex - 1),
+    }));
+  }
+
+  handleNext = () => {
+    const { pages } = this;
+
+    this.setState(({ currentIndex }) => ({
+      currentIndex: Math.min(currentIndex + 1, pages.length - 1),
+    }));
+  }
+
+  handleSwipe = (index) => {
+    this.setState({ currentIndex: index });
+  }
+
+  handleFinish = () => {
+    this.props.dispatch(closeOnboarding());
+  }
+
+  handleKeyUp = ({ key }) => {
+    switch (key) {
+    case 'ArrowLeft':
+      this.handlePrev();
+      break;
+    case 'ArrowRight':
+      this.handleNext();
+      break;
+    }
+  }
+
+  render () {
+    const { currentIndex } = this.state;
+    const { pages } = this;
+
+    return (
+      <div className='introduction'>
+        <ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} className='introduction__pager'>
+          {pages.map((page, i) => (
+            <div key={i} className={classNames('introduction__frame-wrapper', { 'active': i === currentIndex })}>{page}</div>
+          ))}
+        </ReactSwipeableViews>
+
+        <div className='introduction__dots'>
+          {pages.map((_, i) => (
+            <div
+              key={`dot-${i}`}
+              role='button'
+              tabIndex='0'
+              data-index={i}
+              onClick={this.handleDot}
+              className={classNames('introduction__dot', { active: i === currentIndex })}
+            />
+          ))}
+        </div>
+      </div>
+    );
+  }
+
+}