diff options
26 files changed, 1048 insertions, 52 deletions
diff --git a/app/features/onboarding/actionTypes.js b/app/features/onboarding/actionTypes.js new file mode 100644 index 0000000..58dbe4d --- /dev/null +++ b/app/features/onboarding/actionTypes.js @@ -0,0 +1,39 @@ +/** + * The type of (redux) action that continues the onboarding by processing + * the next step. + * + * { + * type: CONTINUE_ONBOARDING + * } + */ +export const CONTINUE_ONBOARDING = Symbol('CONTINUE_ONBOARDING'); + +/** + * The type of (redux) action that sets active onboarding. + * + * { + * type: SET_ACTIVE_ONBOARDING, + * name: string, + * section: string + * } + */ +export const SET_ACTIVE_ONBOARDING = Symbol('SET_ACTIVE_ONBOARDING'); + +/** + * The type of (redux) action that starts Onboarding. + * + * { + * type: START_ONBOARDING, + * section: string + * } + */ +export const START_ONBOARDING = Symbol('START_ONBOARDING'); + +/** + * The type of (redux) action that skips all onboarding. + * + * { + * type: SKIP_ONBOARDING + * } + */ +export const SKIP_ONBOARDING = Symbol('SKIP_ONBOARDING'); diff --git a/app/features/onboarding/actions.js b/app/features/onboarding/actions.js new file mode 100644 index 0000000..30f6d42 --- /dev/null +++ b/app/features/onboarding/actions.js @@ -0,0 +1,70 @@ +// @flow + +import { + CONTINUE_ONBOARDING, + SET_ACTIVE_ONBOARDING, + SKIP_ONBOARDING, + START_ONBOARDING +} from './actionTypes'; + +/** + * Continues the onboarding procedure by activating the next step of the current + * section. + * + * @returns {{ + * type: CONTINUE_ONBOARDING + * }} + */ +export function continueOnboarding() { + return { + type: CONTINUE_ONBOARDING + }; +} + +/** + * Set active onboarding. + * + * @param {string} name - Name of onboarding component. + * @param {string} section - Onboarding section. + * @returns {{ + * type: SET_ACTIVE_ONBOARDING, + * name: string, + * section: string + * }} + */ +export function setActiveOnboarding(name: string, section: string) { + return { + type: SET_ACTIVE_ONBOARDING, + name, + section + }; +} + +/** + * Skips onboarding. + * + * @returns {{ + * type: SKIP_ONBOARDING + * }} + */ +export function skipOnboarding() { + return { + type: SKIP_ONBOARDING + }; +} + +/** + * Start onboarding. + * + * @param {string} section - Onboarding section. + * @returns {{ + * type: START_ONBOARDING, + * section: string + * }} + */ +export function startOnboarding(section: string) { + return { + type: START_ONBOARDING, + section + }; +} diff --git a/app/features/onboarding/components/ConferenceURLSpotlight.js b/app/features/onboarding/components/ConferenceURLSpotlight.js new file mode 100644 index 0000000..178e114 --- /dev/null +++ b/app/features/onboarding/components/ConferenceURLSpotlight.js @@ -0,0 +1,70 @@ +// @flow + +import { Spotlight } from '@atlaskit/onboarding'; + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; + +import { continueOnboarding } from '../actions'; + +type Props = { + + /** + * Redux dispatch. + */ + dispatch: Dispatch<*>; +}; + +/** + * Conference URL Spotlight Component. + */ +class ConferenceURLSpotlight extends Component<Props, *> { + /** + * Initializes a new {@code ComponentURLSpotlight} instance. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this._next = this._next.bind(this); + } + + /** + * Render function of component. + * + * @returns {ReactElement} + */ + render() { + return ( + <Spotlight + actions = { [ + { + onClick: this._next, + text: 'Next' + } + ] } + dialogPlacement = 'bottom center' + target = { 'conference-url' } > + Enter the name (or full URL) of the room you want to join. You + may make a name up, just let others know so they enter the same + name. + </Spotlight> + ); + } + + _next: (*) => void; + + /** + * Close the spotlight component. + * + * @returns {void} + */ + _next() { + this.props.dispatch(continueOnboarding()); + } +} + +export default connect()(ConferenceURLSpotlight); + diff --git a/app/features/onboarding/components/EmailSettingSpotlight.js b/app/features/onboarding/components/EmailSettingSpotlight.js new file mode 100644 index 0000000..ad1237e --- /dev/null +++ b/app/features/onboarding/components/EmailSettingSpotlight.js @@ -0,0 +1,69 @@ +// @flow + +import { Spotlight } from '@atlaskit/onboarding'; + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; + +import { continueOnboarding } from '../actions'; + +type Props = { + + /** + * Redux dispatch. + */ + dispatch: Dispatch<*>; +}; + +/** + * Email Setting Spotlight Component. + */ +class EmailSettingSpotlight extends Component<Props, *> { + /** + * Initializes a new {@code EmailSettingSpotlight} instance. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this._next = this._next.bind(this); + } + + /** + * Render function of component. + * + * @returns {ReactElement} + */ + render() { + return ( + <Spotlight + actions = { [ + { + onClick: this._next, + text: 'Next' + } + ] } + dialogPlacement = 'left top' + target = { 'email-setting' } > + The email you enter here will be part of your user profile and + it will be used to display your stored avatar in gravatar.com . + </Spotlight> + ); + } + + _next: (*) => void; + + /** + * Close the spotlight component. + * + * @returns {void} + */ + _next() { + this.props.dispatch(continueOnboarding()); + } +} + +export default connect()(EmailSettingSpotlight); + diff --git a/app/features/onboarding/components/NameSettingSpotlight.js b/app/features/onboarding/components/NameSettingSpotlight.js new file mode 100644 index 0000000..69abec0 --- /dev/null +++ b/app/features/onboarding/components/NameSettingSpotlight.js @@ -0,0 +1,69 @@ +// @flow + +import { Spotlight } from '@atlaskit/onboarding'; + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; + +import { continueOnboarding } from '../actions'; + +type Props = { + + /** + * Redux dispatch. + */ + dispatch: Dispatch<*>; +}; + +/** + * Name Setting Spotlight Component. + */ +class NameSettingSpotlight extends Component<Props, *> { + /** + * Initializes a new {@code NameSettingSpotlight} instance. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this._next = this._next.bind(this); + } + + /** + * Render function of component. + * + * @returns {ReactElement} + */ + render() { + return ( + <Spotlight + actions = { [ + { + onClick: this._next, + text: 'Next' + } + ] } + dialogPlacement = 'left top' + target = { 'name-setting' } > + This will be your display name, others will see you with this + name. + </Spotlight> + ); + } + + _next: (*) => void; + + /** + * Close the spotlight component. + * + * @returns {void} + */ + _next() { + this.props.dispatch(continueOnboarding()); + } +} + +export default connect()(NameSettingSpotlight); + diff --git a/app/features/onboarding/components/Onboarding.js b/app/features/onboarding/components/Onboarding.js new file mode 100644 index 0000000..31ea1d4 --- /dev/null +++ b/app/features/onboarding/components/Onboarding.js @@ -0,0 +1,66 @@ +// @flow + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; + +import { onboardingComponents, onboardingSteps } from '../../onboarding'; + +type Props = { + + /** + * Redux dispatch. + */ + dispatch: Dispatch<*>; + + /** + * Onboarding Section. + */ + section: string; + + /** + * Active Onboarding. + */ + _activeOnboarding: string; +}; + +/** + * Onboarding Component Entry Point. + */ +class Onboarding extends Component<Props, *> { + /** + * Render function of component. + * + * @returns {ReactElement} + */ + render() { + const { section, _activeOnboarding } = this.props; + const steps = onboardingSteps[section]; + + if (_activeOnboarding && steps.includes(_activeOnboarding)) { + const ActiveOnboarding = onboardingComponents[_activeOnboarding]; + + return <ActiveOnboarding />; + } + + return null; + } +} + +/** + * Maps (parts of) the redux state to the React props. + * + * @param {Object} state - The redux state. + * @returns {{ + * _activeOnboarding: string + * }} + */ +function _mapStateToProps(state: Object) { + return { + _activeOnboarding: state.onboarding.activeOnboarding + }; +} + +export default connect(_mapStateToProps)(Onboarding); + + diff --git a/app/features/onboarding/components/OnboardingModal.js b/app/features/onboarding/components/OnboardingModal.js new file mode 100644 index 0000000..fe38c2d --- /dev/null +++ b/app/features/onboarding/components/OnboardingModal.js @@ -0,0 +1,89 @@ +// @flow + +import { Modal } from '@atlaskit/onboarding'; + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; + +import OnboardingModalImage from '../../../images/onboarding.png'; + +import config from '../../config'; + +import { skipOnboarding, continueOnboarding } from '../actions'; + +type Props = { + + /** + * Redux dispatch. + */ + dispatch: Dispatch<*>; +}; + +/** + * Onboarding Modal Component. + */ +class OnboardingModal extends Component<Props, *> { + /** + * Initializes a new {@code OnboardingModal} instance. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + // Bind event handlers. + this._skip = this._skip.bind(this); + this._next = this._next.bind(this); + } + + /** + * Render function of component. + * + * @returns {ReactElement} + */ + render() { + return ( + <Modal + actions = { [ + { + onClick: this._next, + text: 'Start Tour' + }, + { + onClick: this._skip, + text: 'Skip' + } + ] } + heading = { `Welcome to ${config.appName}` } + image = { OnboardingModalImage } > + <p> Let us show you around!</p> + </Modal> + ); + } + + _next: (*) => void; + + /** + * Close the spotlight component. + * + * @returns {void} + */ + _next() { + this.props.dispatch(continueOnboarding()); + } + + _skip: (*) => void; + + /** + * Skips all the onboardings. + * + * @returns {void} + */ + _skip() { + this.props.dispatch(skipOnboarding()); + } + +} + +export default connect()(OnboardingModal); diff --git a/app/features/onboarding/components/ServerSettingSpotlight.js b/app/features/onboarding/components/ServerSettingSpotlight.js new file mode 100644 index 0000000..dbd6eda --- /dev/null +++ b/app/features/onboarding/components/ServerSettingSpotlight.js @@ -0,0 +1,69 @@ +// @flow + +import { Spotlight } from '@atlaskit/onboarding'; + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; + +import { continueOnboarding } from '../actions'; + +type Props = { + + /** + * Redux dispatch. + */ + dispatch: Dispatch<*>; +}; + +/** + * Server Setting Spotlight Component. + */ +class ServerSettingSpotlight extends Component<Props, *> { + /** + * Initializes a new {@code ServerSettingSpotlight} instance. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this._next = this._next.bind(this); + } + + /** + * Render function of component. + * + * @returns {ReactElement} + */ + render() { + return ( + <Spotlight + actions = { [ + { + onClick: this._next, + text: 'Next' + } + ] } + dialogPlacement = 'left top' + target = { 'server-setting' } > + This will be the server where your conferences will take place. + You can use your own, but you don't need to! + </Spotlight> + ); + } + + _next: (*) => void; + + /** + * Close the spotlight component. + * + * @returns {void} + */ + _next() { + this.props.dispatch(continueOnboarding()); + } +} + +export default connect()(ServerSettingSpotlight); + diff --git a/app/features/onboarding/components/SettingsDrawerSpotlight.js b/app/features/onboarding/components/SettingsDrawerSpotlight.js new file mode 100644 index 0000000..15ad601 --- /dev/null +++ b/app/features/onboarding/components/SettingsDrawerSpotlight.js @@ -0,0 +1,68 @@ +// @flow + +import { Spotlight } from '@atlaskit/onboarding'; + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; + +import { openDrawer } from '../../navbar'; +import { SettingsDrawer } from '../../settings'; + +import { continueOnboarding } from '../actions'; + +type Props = { + + /** + * Redux dispatch. + */ + dispatch: Dispatch<*>; +}; + +/** + * Settings Drawer Spotlight Component. + */ +class SettingsDrawerSpotlight extends Component<Props, *> { + /** + * Initializes a new {@code SettingsDrawerSpotlight} instance. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this._next = this._next.bind(this); + } + + /** + * Render function of component. + * + * @returns {ReactElement} + */ + render() { + return ( + <Spotlight + dialogPlacement = 'right top' + target = { 'settings-drawer-button' } + targetOnClick = { this._next }> + Click here to open the settings drawer. + </Spotlight> + ); + } + + _next: (*) => void; + + /** + * Close the spotlight component and opens Settings Drawer and shows + * onboarding. + * + * @returns {void} + */ + _next() { + this.props.dispatch(openDrawer(SettingsDrawer)); + this.props.dispatch(continueOnboarding()); + } +} + +export default connect()(SettingsDrawerSpotlight); + diff --git a/app/features/onboarding/components/StartMutedTogglesSpotlight.js b/app/features/onboarding/components/StartMutedTogglesSpotlight.js new file mode 100644 index 0000000..aad9af5 --- /dev/null +++ b/app/features/onboarding/components/StartMutedTogglesSpotlight.js @@ -0,0 +1,78 @@ +// @flow + +import { Spotlight } from '@atlaskit/onboarding'; + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; + +import { closeDrawer } from '../../navbar'; + +import { continueOnboarding } from '../actions'; + +type Props = { + + /** + * Redux dispatch. + */ + dispatch: Dispatch<*>; +}; + +/** + * Start Muted Toggles Spotlight Component. + */ +class StartMutedTogglesSpotlight extends Component<Props, *> { + /** + * Initializes a new {@code StartMutedTogglesSpotlight} instance. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this._next = this._next.bind(this); + } + + /** + * Render function of component. + * + * @returns {ReactElement} + */ + render() { + return ( + <Spotlight + actions = { [ + { + onClick: this._next, + text: 'Next' + } + ] } + dialogPlacement = 'left top' + target = { 'start-muted-toggles' } > + You can toggle if you want to start with your audio or video + muted here. This will be applied to all conferences. + </Spotlight> + ); + } + + _next: (*) => void; + + /** + * Close the spotlight component. + * + * @returns {void} + */ + _next() { + const { dispatch } = this.props; + + dispatch(continueOnboarding()); + + // FIXME: find a better way to do this. + setTimeout(() => { + dispatch(closeDrawer()); + }, 300); + } +} + +export default connect()(StartMutedTogglesSpotlight); + diff --git a/app/features/onboarding/components/index.js b/app/features/onboarding/components/index.js new file mode 100644 index 0000000..8b3dded --- /dev/null +++ b/app/features/onboarding/components/index.js @@ -0,0 +1,10 @@ +export { default as ConferenceURLSpotlight } from './ConferenceURLSpotlight'; +export { default as EmailSettingSpotlight } from './EmailSettingSpotlight'; +export { default as NameSettingSpotlight } from './NameSettingSpotlight'; +export { default as Onboarding } from './Onboarding'; +export { default as OnboardingModal } from './OnboardingModal'; +export { default as ServerSettingSpotlight } from './ServerSettingSpotlight'; +export { default as SettingsDrawerSpotlight } from './SettingsDrawerSpotlight'; +export { + default as StartMutedTogglesSpotlight +} from './StartMutedTogglesSpotlight'; diff --git a/app/features/onboarding/constants.js b/app/features/onboarding/constants.js new file mode 100644 index 0000000..34cb1ed --- /dev/null +++ b/app/features/onboarding/constants.js @@ -0,0 +1,35 @@ +// @flow + +import { + OnboardingModal, + ConferenceURLSpotlight, + SettingsDrawerSpotlight, + NameSettingSpotlight, + EmailSettingSpotlight, + ServerSettingSpotlight, + StartMutedTogglesSpotlight +} from './components'; + +export const onboardingSteps = { + 'welcome-page': [ + 'onboarding-modal', + 'conference-url', + 'settings-drawer-button' + ], + 'settings-drawer': [ + 'name-setting', + 'email-setting', + 'server-setting', + 'start-muted-toggles' + ] +}; + +export const onboardingComponents = { + 'onboarding-modal': OnboardingModal, + 'conference-url': ConferenceURLSpotlight, + 'settings-drawer-button': SettingsDrawerSpotlight, + 'name-setting': NameSettingSpotlight, + 'email-setting': EmailSettingSpotlight, + 'server-setting': ServerSettingSpotlight, + 'start-muted-toggles': StartMutedTogglesSpotlight +}; diff --git a/app/features/onboarding/index.js b/app/features/onboarding/index.js new file mode 100644 index 0000000..8446bcd --- /dev/null +++ b/app/features/onboarding/index.js @@ -0,0 +1,7 @@ +export * from './actions'; +export * from './actionTypes'; +export * from './components'; +export * from './constants'; + +export { default as middleware } from './middleware'; +export { default as reducer } from './reducer'; diff --git a/app/features/onboarding/middleware.js b/app/features/onboarding/middleware.js new file mode 100644 index 0000000..36ee2a2 --- /dev/null +++ b/app/features/onboarding/middleware.js @@ -0,0 +1,37 @@ +// @flow + +import { setActiveOnboarding } from './actions'; +import { CONTINUE_ONBOARDING, START_ONBOARDING } from './actionTypes'; +import { onboardingSteps } from './constants'; + +export default (store: Object) => (next: Function) => (action: Object) => { + const result = next(action); + const state = store.getState(); + + switch (action.type) { + case CONTINUE_ONBOARDING: { + const section = state.onboarding.activeOnboardingSection; + + const nextStep = onboardingSteps[section].find( + step => !state.onboarding.onboardingShown.includes(step) + ); + + store.dispatch(setActiveOnboarding(nextStep, nextStep && section)); + break; + } + + case START_ONBOARDING: { + const { section } = action; + const nextStep = onboardingSteps[section].find( + step => !state.onboarding.onboardingShown.includes(step) + ); + + if (nextStep) { + store.dispatch(setActiveOnboarding(nextStep, section)); + } + break; + } + } + + return result; +}; diff --git a/app/features/onboarding/reducer.js b/app/features/onboarding/reducer.js new file mode 100644 index 0000000..59904ce --- /dev/null +++ b/app/features/onboarding/reducer.js @@ -0,0 +1,66 @@ +// @flow + +import { + CONTINUE_ONBOARDING, + SET_ACTIVE_ONBOARDING, + SKIP_ONBOARDING +} from './actionTypes'; +import { onboardingSteps } from './constants'; + +type State = { + activeOnboarding: ?string; + activeOnboardingSection: ?string; + onboardingShown: Array<string>; +}; + +const DEFAULT_STATE = { + activeOnboarding: undefined, + activeOnboardingSection: undefined, + onboardingShown: [] +}; + +/** + * Reduces redux actions for features/onboarding. + * + * @param {State} state - Current reduced redux state. + * @param {Object} action - Action which was dispatched. + * @returns {State} - Updated reduced redux state. + */ +export default (state: State = DEFAULT_STATE, action: Object) => { + switch (action.type) { + case CONTINUE_ONBOARDING: + return { + ...state, + activeOnboarding: undefined, + onboardingShown: + + // $FlowFixMe + state.onboardingShown.concat(state.activeOnboarding) + }; + + case SET_ACTIVE_ONBOARDING: + return { + ...state, + activeOnboarding: action.name, + activeOnboardingSection: action.section + }; + + case SKIP_ONBOARDING: { + // $FlowFixMe + const allSteps = [].concat(...Object.values(onboardingSteps)); + + return { + ...state, + activeOnboarding: undefined, + activeOnboardingSection: undefined, + onboardingShown: + + // $FlowFixMe + state.onboardingShown.concat(allSteps) + }; + } + + default: + return state; + } +}; diff --git a/app/features/redux/middleware.js b/app/features/redux/middleware.js index 9938da5..86d7acc 100644 --- a/app/features/redux/middleware.js +++ b/app/features/redux/middleware.js @@ -3,10 +3,12 @@ import { applyMiddleware } from 'redux'; import { createLogger } from 'redux-logger'; +import { middleware as onboardingMiddleware } from '../onboarding'; import { middleware as routerMiddleware } from '../router'; import { middleware as settingsMiddleware } from '../settings'; export default applyMiddleware( + onboardingMiddleware, routerMiddleware, settingsMiddleware, createLogger() diff --git a/app/features/redux/reducers.js b/app/features/redux/reducers.js index 2f532e6..d72eb54 100644 --- a/app/features/redux/reducers.js +++ b/app/features/redux/reducers.js @@ -3,12 +3,14 @@ import { combineReducers } from 'redux'; import { reducer as navbarReducer } from '../navbar'; +import { reducer as onboardingReducer } from '../onboarding'; import { reducer as recentListReducer } from '../recent-list'; import { reducer as routerReducer } from '../router'; import { reducer as settingsReducer } from '../settings'; export default combineReducers({ navbar: navbarReducer, + onboarding: onboardingReducer, recentList: recentListReducer, router: routerReducer, settings: settingsReducer diff --git a/app/features/redux/store.js b/app/features/redux/store.js index 472cdbd..b9469b9 100644 --- a/app/features/redux/store.js +++ b/app/features/redux/store.js @@ -11,6 +11,7 @@ const persistConfig = { key: 'root', storage: createElectronStorage(), whitelist: [ + 'onboarding', 'recentList', 'settings' ] diff --git a/app/features/settings/components/SettingsButton.js b/app/features/settings/components/SettingsButton.js index bb0115c..bd80f92 100644 --- a/app/features/settings/components/SettingsButton.js +++ b/app/features/settings/components/SettingsButton.js @@ -1,6 +1,7 @@ // @flow import SettingsIcon from '@atlaskit/icon/glyph/settings'; +import { SpotlightTarget } from '@atlaskit/onboarding'; import * as Mousetrap from 'mousetrap'; import 'mousetrap/plugins/global-bind/mousetrap-global-bind'; @@ -66,8 +67,11 @@ class SettingsButton extends Component<Props, *> { */ render() { return ( - <SettingsIcon - onClick = { this._onIconClick } /> + <SpotlightTarget + name = 'settings-drawer-button'> + <SettingsIcon + onClick = { this._onIconClick } /> + </SpotlightTarget> ); } diff --git a/app/features/settings/components/SettingsDrawer.js b/app/features/settings/components/SettingsDrawer.js index 7990e65..6ecf7eb 100644 --- a/app/features/settings/components/SettingsDrawer.js +++ b/app/features/settings/components/SettingsDrawer.js @@ -4,12 +4,14 @@ import Avatar from '@atlaskit/avatar'; import FieldText from '@atlaskit/field-text'; import ArrowLeft from '@atlaskit/icon/glyph/arrow-left'; import { AkCustomDrawer } from '@atlaskit/navigation'; +import { SpotlightTarget } from '@atlaskit/onboarding'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import type { Dispatch } from 'redux'; import { closeDrawer, DrawerContainer, Logo } from '../../navbar'; +import { Onboarding, startOnboarding } from '../../onboarding'; import { AvatarContainer, SettingsContainer } from '../styled'; import { setEmail, setName } from '../actions'; @@ -64,6 +66,25 @@ class SettingsDrawer extends Component<Props, *> { } /** + * Start Onboarding once component is mounted. + * + * NOTE: It automatically checks if the onboarding is shown or not. + * + * @param {Props} prevProps - Props before component updated. + * @returns {void} + */ + componentDidUpdate(prevProps: Props) { + if (!prevProps.isOpen && this.props.isOpen) { + + // TODO - Find a better way for this. + // Delay for 300ms to let drawer open. + setTimeout(() => { + this.props.dispatch(startOnboarding('settings-drawer')); + }, 300); + } + } + + /** * Render function of component. * * @returns {ReactElement} @@ -82,24 +103,37 @@ class SettingsDrawer extends Component<Props, *> { size = 'xlarge' src = { this.props._avatarURL } /> </AvatarContainer> - <form onSubmit = { this._onNameFormSubmit }> - <FieldText - label = 'Name' - onBlur = { this._onNameBlur } - shouldFitContainer = { true } - type = 'text' - value = { this.props._name } /> - </form> - <form onSubmit = { this._onEmailFormSubmit }> - <FieldText - label = 'Email' - onBlur = { this._onEmailBlur } - shouldFitContainer = { true } - type = 'text' - value = { this.props._email } /> - </form> - <ServerURLField /> - <StartMutedToggles /> + <SpotlightTarget + name = 'name-setting'> + <form onSubmit = { this._onNameFormSubmit }> + <FieldText + label = 'Name' + onBlur = { this._onNameBlur } + shouldFitContainer = { true } + type = 'text' + value = { this.props._name } /> + </form> + </SpotlightTarget> + <SpotlightTarget + name = 'email-setting'> + <form onSubmit = { this._onEmailFormSubmit }> + <FieldText + label = 'Email' + onBlur = { this._onEmailBlur } + shouldFitContainer = { true } + type = 'text' + value = { this.props._email } /> + </form> + </SpotlightTarget> + <SpotlightTarget + name = 'server-setting'> + <ServerURLField /> + </SpotlightTarget> + <SpotlightTarget + name = 'start-muted-toggles'> + <StartMutedToggles /> + </SpotlightTarget> + <Onboarding section = 'settings-drawer' /> </SettingsContainer> </DrawerContainer> </AkCustomDrawer> diff --git a/app/features/welcome/components/Welcome.js b/app/features/welcome/components/Welcome.js index 519016d..c9a35f8 100644 --- a/app/features/welcome/components/Welcome.js +++ b/app/features/welcome/components/Welcome.js @@ -2,6 +2,7 @@ import Button from '@atlaskit/button'; import { FieldTextStateless } from '@atlaskit/field-text'; +import { SpotlightTarget } from '@atlaskit/onboarding'; import Page from '@atlaskit/page'; import { AtlasKitThemeProvider } from '@atlaskit/theme'; @@ -11,6 +12,7 @@ import { connect } from 'react-redux'; import { push } from 'react-router-redux'; import { Navbar } from '../../navbar'; +import { Onboarding, startOnboarding } from '../../onboarding'; import { RecentList } from '../../recent-list'; import { normalizeServerURL } from '../../utils'; @@ -72,6 +74,17 @@ class Welcome extends Component<Props, State> { } /** + * Start Onboarding once component is mounted. + * + * NOTE: It autonatically checks if the onboarding is shown or not. + * + * @returns {void} + */ + componentDidMount() { + this.props.dispatch(startOnboarding('welcome-page')); + } + + /** * Render function of component. * * @returns {ReactElement} @@ -84,16 +97,18 @@ class Welcome extends Component<Props, State> { <AtlasKitThemeProvider mode = 'light'> <Wrapper> <Header> - <Form onSubmit = { this._onFormSubmit }> - <FieldTextStateless - autoFocus = { true } - isInvalid = { state && state.error } - isLabelHidden = { true } - onChange = { this._onURLChange } - shouldFitContainer = { true } - type = 'text' - value = { this.state.url } /> - </Form> + <SpotlightTarget name = 'conference-url'> + <Form onSubmit = { this._onFormSubmit }> + <FieldTextStateless + autoFocus = { true } + isInvalid = { state && state.error } + isLabelHidden = { true } + onChange = { this._onURLChange } + shouldFitContainer = { true } + type = 'text' + value = { this.state.url } /> + </Form> + </SpotlightTarget> <Button appearance = 'primary' onClick = { this._onJoin } @@ -104,6 +119,7 @@ class Welcome extends Component<Props, State> { <Body> <RecentList /> </Body> + <Onboarding section = 'welcome-page' /> </Wrapper> </AtlasKitThemeProvider> </Page> diff --git a/app/images/onboarding.png b/app/images/onboarding.png Binary files differnew file mode 100644 index 0000000..604cd04 --- /dev/null +++ b/app/images/onboarding.png diff --git a/app/index.js b/app/index.js index 215fca2..d4dad57 100644 --- a/app/index.js +++ b/app/index.js @@ -5,6 +5,8 @@ */ import '@atlaskit/css-reset'; +import { SpotlightManager } from '@atlaskit/onboarding'; + import React, { Component } from 'react'; import { render } from 'react-dom'; import { Provider } from 'react-redux'; @@ -28,7 +30,9 @@ class Root extends Component<*> { <PersistGate loading = { null } persistor = { persistor }> - <App /> + <SpotlightManager> + <App /> + </SpotlightManager> </PersistGate> </Provider> ); diff --git a/package-lock.json b/package-lock.json index 22b7b53..bbcc89d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -182,6 +182,23 @@ "react-transition-group": "^2.2.1" } }, + "@atlaskit/modal-dialog": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@atlaskit/modal-dialog/-/modal-dialog-6.0.3.tgz", + "integrity": "sha512-qyM4l4jr+uYrb0qlme6g5AQwBJL7qBbR6lNe6ZiFOU7sxiC45DNwO36sUIpbJy4De8Ld5bwahKx6kBdu2fQVLw==", + "requires": { + "@atlaskit/analytics-next": "^3.0.1", + "@atlaskit/blanket": "^7.0.1", + "@atlaskit/button": "^9.0.2", + "@atlaskit/icon": "^13.0.0", + "@atlaskit/layer-manager": "^5.0.0", + "@atlaskit/theme": "^5.0.0", + "prop-types": "^15.5.10", + "raf-schd": "^2.1.0", + "react-scrolllock": "^3.0.1", + "react-transition-group": "^2.2.1" + } + }, "@atlaskit/navigation": { "version": "33.0.2", "resolved": "https://registry.npmjs.org/@atlaskit/navigation/-/navigation-33.0.2.tgz", @@ -212,6 +229,26 @@ "react-transition-group": "^2.2.1" } }, + "@atlaskit/onboarding": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@atlaskit/onboarding/-/onboarding-5.1.1.tgz", + "integrity": "sha512-kMgQMeQJROFM3N0A2Uenn6yl6UYaCrSDekzM/ImnPkIEUnNVw8KD9Ok26ioi//FKmTjG6GOLYK6FKIbxtg2RvA==", + "requires": { + "@atlaskit/analytics-next": "^3.0.1", + "@atlaskit/button": "^9.0.2", + "@atlaskit/icon": "^13.0.0", + "@atlaskit/layer": "^5.0.0", + "@atlaskit/layer-manager": "^5.0.0", + "@atlaskit/modal-dialog": "^6.0.1", + "@atlaskit/theme": "^5.0.0", + "prop-types": "^15.5.10", + "react-lorem-component": "^0.12.2", + "react-node-resolver": "^1.0.1", + "react-scrolllock": "^3.0.1", + "react-transition-group": "^2.2.1", + "unstated": "^1.2.0" + } + }, "@atlaskit/page": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@atlaskit/page/-/page-8.0.0.tgz", @@ -3142,6 +3179,21 @@ "sha.js": "^2.4.8" } }, + "create-react-class": { + "version": "15.6.3", + "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz", + "integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==", + "requires": { + "fbjs": "^0.8.9", + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" + } + }, + "create-react-context": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.1.6.tgz", + "integrity": "sha512-eCnYYEUEc5i32LHwpE/W7NlddOB9oHwsPaWtWzYtflNkkwa3IfindIcoXdVWs12zCbwaMCavKNu84EXogVIWHw==" + }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -4765,6 +4817,16 @@ "object-assign": "^4.0.1" } }, + "file-loader": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz", + "integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==", + "dev": true, + "requires": { + "loader-utils": "^1.0.2", + "schema-utils": "^0.4.5" + } + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -4994,14 +5056,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5016,20 +5076,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -5146,8 +5203,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5159,7 +5215,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5174,7 +5229,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5182,14 +5236,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -5208,7 +5260,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5289,8 +5340,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5302,7 +5352,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5424,7 +5473,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6848,6 +6896,14 @@ "js-tokens": "^3.0.0" } }, + "lorem-ipsum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lorem-ipsum/-/lorem-ipsum-1.0.5.tgz", + "integrity": "sha512-GW1N2mqtS1qRf4WHmQ/g/asGXXd2dciLUDEsz5hZQfVJNWHUZX0zsJDLfV581de4OOCrBRiTifyoSuSSUwIEdg==", + "requires": { + "minimist": "~1.2.0" + } + }, "loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", @@ -8143,6 +8199,22 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-lorem-component": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/react-lorem-component/-/react-lorem-component-0.12.2.tgz", + "integrity": "sha1-oR/SmLBEos8GKQXFOYiA5Gv+zCU=", + "requires": { + "create-react-class": "^15.5.3", + "lorem-ipsum": "^1.0.3", + "object-assign": "^4.1.0", + "seedable-random": "0.0.1" + } + }, + "react-node-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/react-node-resolver/-/react-node-resolver-1.0.1.tgz", + "integrity": "sha1-F5inKcDiGL8vDo3fecVQ1K9h2Do=" + }, "react-redux": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.7.tgz", @@ -8775,6 +8847,11 @@ } } }, + "seedable-random": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/seedable-random/-/seedable-random-0.0.1.tgz", + "integrity": "sha1-CzDOp55DmWiMWgZ1A6Bmt8QegxY=" + }, "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", @@ -9950,6 +10027,14 @@ } } }, + "unstated": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/unstated/-/unstated-1.2.0.tgz", + "integrity": "sha512-nmI65VVuMRFm1UBxF1BEWTt8XoRldX1gEwcyBhcFJSsLycHuHFa8qjYnTv8wMISGs7e+HKWeXAtTi1DvEsg00w==", + "requires": { + "create-react-context": "^0.1.5" + } + }, "unzip-response": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", diff --git a/package.json b/package.json index ed1ba66..37812f0 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "@atlaskit/field-text": "7.0.1", "@atlaskit/icon": "13.2.0", "@atlaskit/navigation": "33.0.2", + "@atlaskit/onboarding": "5.1.1", "@atlaskit/page": "8.0.0", "@atlaskit/spinner": "9.0.2", "@atlaskit/theme": "5.1.0", @@ -124,6 +125,7 @@ "eslint-plugin-import": "2.13.0", "eslint-plugin-jsdoc": "3.7.1", "eslint-plugin-react": "7.10.0", + "file-loader": "1.1.11", "flow-bin": "0.76.0", "html-webpack-plugin": "3.2.0", "precommit-hook": "3.0.0", diff --git a/webpack.config.js b/webpack.config.js index 954cbb0..56e5140 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -45,6 +45,10 @@ const commonConfig = { test: /\.css$/ }, { + use: 'file-loader', + test: /\.png$/ + }, + { loader: 'svg-inline-loader', test: /\.svg$/ } |