MM-25003_Improve Onboarding screens for the desktop app - Intro Screen (#2220)

This commit is contained in:
Julian Mondragón
2022-08-16 12:33:03 -05:00
committed by GitHub
parent d4282d965e
commit faf2dae74b
50 changed files with 1815 additions and 42 deletions

View File

@@ -0,0 +1,137 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState, useEffect, useRef} from 'react';
import classNames from 'classnames';
import CarouselButton, {ButtonDirection} from './CarouselButton';
import CarouselPaginationIndicator from './CarouselPaginationIndicator';
import 'renderer/css/components/Carousel.scss';
const AUTO_CHANGE_TIME = 5000;
type CarouselProps = {
slides: Array<{key: string; content: React.ReactNode}>;
startIndex?: number;
darkMode?: boolean;
};
function Carousel({
slides,
startIndex = 0,
darkMode = false,
}: CarouselProps) {
const [slideIn, setSlideIn] = useState(startIndex);
const [slideOut, setSlideOut] = useState(NaN);
const [direction, setDirection] = useState(ButtonDirection.NEXT);
const [autoChange, setAutoChange] = useState(true);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const disableNavigation = slides.length <= 1;
useEffect(() => {
timerRef.current = autoChange ? (
setTimeout(() => {
handleOnNextButtonClick(true);
}, AUTO_CHANGE_TIME)
) : null;
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, [slideIn, autoChange]);
const handleOnPrevButtonClick = () => {
moveSlide(slideIn - 1);
setDirection(ButtonDirection.PREV);
setAutoChange(false);
};
const handleOnNextButtonClick = (fromAuto?: boolean) => {
moveSlide(slideIn + 1);
setDirection(ButtonDirection.NEXT);
if (!fromAuto) {
setAutoChange(false);
}
};
const handleOnPaginationIndicatorClick = (indicatorIndex: number) => {
moveSlide(indicatorIndex);
setDirection(indicatorIndex > slideIn ? ButtonDirection.NEXT : ButtonDirection.PREV);
setAutoChange(false);
};
const moveSlide = (toIndex: number) => {
if (toIndex === slideIn) {
return;
}
let current = toIndex;
if (toIndex < 0) {
current = slides.length - 1;
} else if (toIndex >= slides.length) {
current = 0;
}
setSlideOut(slideIn);
setSlideIn(current);
};
return (
<div className='Carousel'>
<div className='Carousel__slides'>
{slides.map(({key, content}, slideIndex) => {
const isPrev = slideIndex === slideOut;
const isCurrent = slideIndex === slideIn;
return (
<div
key={key}
id={key}
className={classNames(
'Carousel__slide',
{
'Carousel__slide-current': isCurrent,
inFromRight: isCurrent && direction === ButtonDirection.NEXT,
inFromLeft: isCurrent && direction === ButtonDirection.PREV,
outToLeft: isPrev && direction === ButtonDirection.NEXT,
outToRight: isPrev && direction === ButtonDirection.PREV,
},
)}
>
{content}
</div>
);
})}
</div>
<div className='Carousel__pagination'>
<CarouselButton
direction={ButtonDirection.PREV}
disabled={disableNavigation}
darkMode={darkMode}
onClick={handleOnPrevButtonClick}
/>
<CarouselPaginationIndicator
pages={slides.length}
activePage={slideIn}
disabled={disableNavigation}
darkMode={darkMode}
onClick={handleOnPaginationIndicatorClick}
/>
<CarouselButton
direction={ButtonDirection.NEXT}
disabled={disableNavigation}
darkMode={darkMode}
onClick={handleOnNextButtonClick}
/>
</div>
</div>
);
}
export default Carousel;

View File

@@ -0,0 +1,51 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import classNames from 'classnames';
import 'renderer/css/components/Button.scss';
import 'renderer/css/components/CarouselButton.scss';
export enum ButtonDirection {
NEXT = 'next',
PREV = 'prev',
}
type CarouselButtonProps = {
direction: ButtonDirection;
disabled?: boolean;
darkMode?: boolean;
onClick?: () => void;
};
function CarouselButton({
direction = ButtonDirection.NEXT,
disabled = false,
darkMode = false,
onClick = () => null,
}: CarouselButtonProps) {
const handleOnClick = () => {
onClick();
};
return (
<button
id={`${direction}CarouselButton`}
className={classNames(
'CarouselButton',
'icon-button icon-button-small',
{
'icon-button-inverted': darkMode,
disabled,
},
)}
disabled={disabled}
onClick={handleOnClick}
>
<i className={direction === ButtonDirection.PREV ? 'icon-chevron-left' : 'icon-chevron-right'}/>
</button>
);
}
export default CarouselButton;

View File

@@ -0,0 +1,74 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import classNames from 'classnames';
import 'renderer/css/components/Button.scss';
import 'renderer/css/components/CarouselPaginationIndicator.scss';
type CarouselPaginationIndicatorProps = {
pages: number;
activePage: number;
disabled?: boolean;
darkMode?: boolean;
onClick?: (pageIndex: number) => void;
};
function CarouselPaginationIndicator({
pages,
activePage,
disabled,
darkMode,
onClick = () => null,
}: CarouselPaginationIndicatorProps) {
const handleOnClick = useCallback((pageIndex: number) => () => {
onClick(pageIndex);
}, [onClick]);
const handleOnKeyDown = useCallback((pageIndex: number) => (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
onClick(pageIndex);
}
}, [onClick]);
const getIndicators = useCallback(() => {
const indicators = [];
for (let pageIndex = 0; pageIndex < pages; pageIndex++) {
indicators.push(
<div
key={pageIndex}
id={`PaginationIndicator${pageIndex}`}
onClick={handleOnClick(pageIndex)}
onKeyDown={handleOnKeyDown(pageIndex)}
className={classNames(
'indicatorDot',
{
'indicatorDot-inverted': darkMode,
active: activePage === pageIndex,
disabled,
},
)}
role='button'
tabIndex={0}
>
<div className='dot'/>
</div>,
);
}
return indicators;
}, [pages, activePage, darkMode, handleOnClick]);
return (
<div
className='CarouselPaginationIndicator'
tabIndex={-1}
>
{getIndicators()}
</div>
);
}
export default CarouselPaginationIndicator;

View File

@@ -0,0 +1,4 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export {default} from './Carousel';

View File

@@ -0,0 +1,35 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import classNames from 'classnames';
import Logo from 'renderer/components/Logo';
import 'renderer/css/components/Header.scss';
type HeaderProps = {
alternateLink?: React.ReactElement;
darkMode?: boolean;
}
const Header = ({
alternateLink,
darkMode,
}: HeaderProps) => (
<div
className={classNames(
'Header',
{'Header--darkMode': darkMode},
)}
>
<div className='Header__main'>
<div className='Header__logo'>
<Logo/>
</div>
{alternateLink}
</div>
</div>
);
export default Header;

View File

@@ -0,0 +1,4 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export {default} from './Header';

File diff suppressed because one or more lines are too long

View File

@@ -521,6 +521,11 @@ class MainPage extends React.PureComponent<Props, State> {
ref={this.topBar}
className={'topBar-bg'}
>
{window.process.platform !== 'linux' && this.props.teams.length === 0 && (
<div className='app-title'>
{intl.formatMessage({id: 'renderer.components.mainPage.titleBar', defaultMessage: 'Mattermost'})}
</div>
)}
<button
className='three-dot-menu'
onClick={this.openMenu}
@@ -530,14 +535,16 @@ class MainPage extends React.PureComponent<Props, State> {
>
<i className='icon-dots-vertical'/>
</button>
<TeamDropdownButton
isDisabled={this.state.modalOpen}
activeServerName={this.state.activeServerName}
totalMentionCount={totalMentionCount}
hasUnreads={totalUnreadCount > 0}
isMenuOpen={this.state.isMenuOpen}
darkMode={this.state.darkMode}
/>
{this.props.teams.length !== 0 && (
<TeamDropdownButton
isDisabled={this.state.modalOpen}
activeServerName={this.state.activeServerName}
totalMentionCount={totalMentionCount}
hasUnreads={totalUnreadCount > 0}
isMenuOpen={this.state.isMenuOpen}
darkMode={this.state.darkMode}
/>
)}
{tabsRow}
{upgradeIcon}
{titleBarButtons}

View File

@@ -0,0 +1,149 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {useIntl, FormattedMessage} from 'react-intl';
import classNames from 'classnames';
import bullseye from 'renderer/assets/svg/bullseye.svg';
import channels from 'renderer/assets/svg/channels.svg';
import chat2 from 'renderer/assets/svg/chat2.svg';
import clipboard from 'renderer/assets/svg/clipboard.svg';
import Carousel from 'renderer/components/Carousel';
import Header from 'renderer/components/Header';
import LoadingBackground from 'renderer/components/LoadingScreen/LoadingBackground';
import WelcomeScreenSlide from './WelcomeScreenSlide';
import 'renderer/css/components/Button.scss';
import 'renderer/css/components/WelcomeScreen.scss';
import 'renderer/css/components/LoadingScreen.css';
type WelcomeScreenProps = {
darkMode?: boolean;
onGetStarted?: () => void;
};
function WelcomeScreen({
darkMode = false,
onGetStarted = () => null,
}: WelcomeScreenProps) {
const {formatMessage} = useIntl();
const slides = useMemo(() => [
{
key: 'welcome',
title: formatMessage({id: 'renderer.components.welcomeScreen.slides.welcome.title', defaultMessage: 'Welcome'}),
subtitle: formatMessage({
id: 'renderer.components.welcomeScreen.slides.welcome.subtitle',
defaultMessage: 'Mattermost is an open source platform for developer collaboration. Secure, flexible, and integrated with the tools you love.',
}),
image: (
<img
src={chat2}
draggable={false}
/>
),
main: true,
},
{
key: 'channels',
title: formatMessage({id: 'renderer.components.welcomeScreen.slides.channels.title', defaultMessage: 'Channels'}),
subtitle: (
<FormattedMessage
id='renderer.components.welcomeScreen.slides.channels.subtitle'
defaultMessage='All of your teams communication in one place.<br></br>Secure collaboration, built for developers.'
values={{
br: (x: React.ReactNode) => (<><br/>{x}</>),
}}
/>
),
image: (
<img
src={channels}
draggable={false}
/>
),
},
{
key: 'playbooks',
title: formatMessage({id: 'renderer.components.welcomeScreen.slides.playbooks.title', defaultMessage: 'Playbooks'}),
subtitle: formatMessage({
id: 'renderer.components.welcomeScreen.slides.palybooks.subtitle',
defaultMessage: 'Move faster and make fewer mistakes with checklists, automations, and tool integrations that power your teams workflows.',
}),
image: (
<img
src={clipboard}
draggable={false}
/>
),
},
{
key: 'boards',
title: formatMessage({id: 'renderer.components.welcomeScreen.slides.boards.title', defaultMessage: 'Boards'}),
subtitle: formatMessage({
id: 'renderer.components.welcomeScreen.slides.boards.subtitle',
defaultMessage: 'Ship on time, every time, with a project and task management solution built for digital operations.',
}),
image: (
<img
src={bullseye}
draggable={false}
/>
),
},
], []);
const handleOnGetStartedClick = () => {
onGetStarted();
};
return (
<div
className={classNames(
'LoadingScreen',
{'LoadingScreen--darkMode': darkMode},
'WelcomeScreen',
)}
>
<LoadingBackground/>
<Header darkMode={darkMode}/>
<div className='WelcomeScreen__body'>
<div className='WelcomeScreen__content'>
<Carousel
slides={slides.map(({key, title, subtitle, image, main}) => ({
key,
content: (
<WelcomeScreenSlide
key={key}
title={title}
subtitle={subtitle}
image={image}
isMain={main}
darkMode={darkMode}
/>
),
}))}
darkMode={darkMode}
/>
<button
id='getStartedWelcomeScreen'
className={classNames(
'WelcomeScreen__button',
'primary-button primary-medium-button',
{'primary-button-inverted': darkMode},
)}
onClick={handleOnGetStartedClick}
>
{formatMessage({id: 'renderer.components.welcomeScreen.button.getStarted', defaultMessage: 'Get Started'})}
</button>
</div>
</div>
<div className='WelcomeScreen__footer'/>
</div>
);
}
export default WelcomeScreen;

View File

@@ -0,0 +1,45 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import classNames from 'classnames';
import 'renderer/css/components/WelcomeScreenSlide.scss';
type WelcomeScreenSlideProps = {
title: string;
subtitle: string | React.ReactElement;
image: React.ReactNode;
isMain?: boolean;
darkMode?: boolean;
};
const WelcomeScreenSlide = ({
title,
subtitle,
image,
isMain,
darkMode,
}: WelcomeScreenSlideProps) => (
<div
className={classNames(
'WelcomeScreenSlide',
{
'WelcomeScreenSlide--main': isMain,
'WelcomeScreenSlide--darkMode': darkMode,
},
)}
>
<div className='WelcomeScreenSlide__image'>
{image}
</div>
<div className='WelcomeScreenSlide__title'>
{title}
</div>
<div className='WelcomeScreenSlide__subtitle'>
{subtitle}
</div>
</div>
);
export default WelcomeScreenSlide;

View File

@@ -0,0 +1,4 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export {default} from './WelcomeScreen';