MM-25003_Improve Onboarding screens for the desktop app - Intro Screen (#2220)
This commit is contained in:
137
src/renderer/components/Carousel/Carousel.tsx
Normal file
137
src/renderer/components/Carousel/Carousel.tsx
Normal 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;
|
51
src/renderer/components/Carousel/CarouselButton.tsx
Normal file
51
src/renderer/components/Carousel/CarouselButton.tsx
Normal 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;
|
@@ -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;
|
4
src/renderer/components/Carousel/index.ts
Normal file
4
src/renderer/components/Carousel/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export {default} from './Carousel';
|
35
src/renderer/components/Header/Header.tsx
Normal file
35
src/renderer/components/Header/Header.tsx
Normal 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;
|
4
src/renderer/components/Header/index.ts
Normal file
4
src/renderer/components/Header/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export {default} from './Header';
|
35
src/renderer/components/Logo.tsx
Normal file
35
src/renderer/components/Logo.tsx
Normal file
File diff suppressed because one or more lines are too long
@@ -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}
|
||||
|
149
src/renderer/components/WelcomeScreen/WelcomeScreen.tsx
Normal file
149
src/renderer/components/WelcomeScreen/WelcomeScreen.tsx
Normal 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 team’s 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 team’s 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;
|
45
src/renderer/components/WelcomeScreen/WelcomeScreenSlide.tsx
Normal file
45
src/renderer/components/WelcomeScreen/WelcomeScreenSlide.tsx
Normal 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;
|
4
src/renderer/components/WelcomeScreen/index.ts
Normal file
4
src/renderer/components/WelcomeScreen/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export {default} from './WelcomeScreen';
|
Reference in New Issue
Block a user