Remove saga from Onboarding container and simplified the logic a little

Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
soupette 2020-02-11 16:45:10 +01:00
parent 825721713a
commit 41d649f725
15 changed files with 174 additions and 629 deletions

View File

@ -30,8 +30,7 @@ import LocaleToggle from '../LocaleToggle';
import HomePage from '../HomePage';
import Marketplace from '../Marketplace';
import NotFoundPage from '../NotFoundPage';
// import Onboarding from '../Onboarding';
import OnboardingVideos from '../OnboardingVideos';
import OnboardingVideos from '../Onboarding';
import SettingsPage from '../SettingsPage';
import PluginDispatcher from '../PluginDispatcher';
import {

View File

@ -7,6 +7,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import { isNaN } from 'lodash';
import { Modal, ModalHeader, ModalBody } from 'reactstrap';
import { Player } from 'video-react';
@ -17,7 +18,9 @@ import Li from './Li';
class OnboardingVideo extends React.Component {
componentDidMount() {
this.hiddenPlayer.current.subscribeToStateChange(this.handleChangeState);
if (this.hiddenPlayer.current) {
this.hiddenPlayer.current.subscribeToStateChange(this.handleChangeState);
}
}
hiddenPlayer = React.createRef();
@ -28,7 +31,7 @@ class OnboardingVideo extends React.Component {
const { duration } = state;
const { id } = this.props;
if (duration !== prevState.duration) {
if (duration !== prevState.duration && !isNaN(duration)) {
this.props.setVideoDuration(id, duration);
}
};

View File

@ -32,6 +32,7 @@ const fadeOut = keyframes`
`;
const Wrapper = styled.div`
max-width: ${({ isOpen }) => (isOpen ? null : '0px')};
position: fixed;
right: 15px;
bottom: 15px;

View File

@ -1,64 +0,0 @@
/*
*
* Onboarding actions
*
*/
import { GET_VIDEOS, GET_VIDEOS_SUCCEEDED, SHOULD_OPEN_MODAL, ON_CLICK, SET_VIDEOS_DURATION, UPDATE_VIDEO_START_TIME, SET_VIDEO_END, REMOVE_VIDEOS } from './constants';
export function getVideos() {
return {
type: GET_VIDEOS,
};
}
export function getVideosSucceeded(videos) {
return {
type: GET_VIDEOS_SUCCEEDED,
videos,
};
}
export function shouldOpenModal(opened) {
return {
type: SHOULD_OPEN_MODAL,
opened,
};
}
export function onClick(e) {
return {
type: ON_CLICK,
index: parseInt(e.currentTarget.id, 10),
};
}
export function setVideoDuration(index, duration) {
return {
type: SET_VIDEOS_DURATION,
index: parseInt(index, 10),
duration: parseFloat(duration, 10),
};
}
export function updateVideoStartTime(index, startTime) {
return {
type: UPDATE_VIDEO_START_TIME,
index: parseInt(index, 10),
startTime: parseFloat(startTime, 10),
};
}
export function setVideoEnd(index, end) {
return {
type: SET_VIDEO_END,
index: parseInt(index, 10),
end,
};
}
export function removeVideos() {
return {
type: REMOVE_VIDEOS,
};
}

View File

@ -1,15 +0,0 @@
/*
*
* Onboarding constants
*
*/
export const GET_VIDEOS = 'StrapiAdmin/Onboarding/GET_VIDEOS';
export const GET_VIDEOS_SUCCEEDED =
'StrapiAdmin/Onboarding/GET_VIDEOS_SUCCEEDED';
export const SHOULD_OPEN_MODAL = 'StrapiAdmin/Onboarding/SHOULD_OPEN_MODAL';
export const ON_CLICK = 'StrapiAdmin/Onboarding/ON_CLICK';
export const SET_VIDEOS_DURATION = 'StrapiAdmin/Onboarding/SET_VIDEOS_DURATION';
export const UPDATE_VIDEO_START_TIME = 'StrapiAdmin/Onboarding/UPDATE_VIDEO_START_TIME';
export const SET_VIDEO_END = 'StrapiAdmin/Onboarding/SET_VIDEO_END';
export const REMOVE_VIDEOS = 'StrapiAdmin/Onboarding/REMOVE_VIDEOS';

View File

@ -1,217 +1,136 @@
/**
*
* Onboarding
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import React, { useEffect, useReducer, memo } from 'react';
import { FormattedMessage } from 'react-intl';
import { GlobalContext } from 'strapi-helper-plugin';
import injectSaga from '../../utils/injectSaga';
import injectReducer from '../../utils/injectReducer';
import OnboardingVideo from '../../components/OnboardingVideo';
import axios from 'axios';
import cn from 'classnames';
import { useGlobalContext } from 'strapi-helper-plugin';
import formatVideoArray from './utils/formatAndStoreVideoArray';
import Video from './Video';
import Wrapper from './Wrapper';
import {
getVideos,
onClick,
removeVideos,
setVideoDuration,
setVideoEnd,
updateVideoStartTime,
} from './actions';
import makeSelectOnboarding from './selectors';
import reducer from './reducer';
import saga from './saga';
import init from './init';
import reducer, { initialState } from './reducer';
/* eslint-disable react/no-array-index-key */
const OnboardingVideos = () => {
const { emitEvent } = useGlobalContext();
const [reducerState, dispatch] = useReducer(reducer, initialState, init);
const { isLoading, isOpen, videos } = reducerState.toJS();
export class Onboarding extends React.Component {
state = { showVideos: false };
useEffect(() => {
const getData = async () => {
try {
const { data } = await axios.get('https://strapi.io/videos', {
timeout: 1000,
});
const { didWatchVideos, videos } = formatVideoArray(data);
componentDidMount() {
this.props.getVideos();
dispatch({
type: 'GET_DATA_SUCCEEDED',
didWatchVideos,
videos,
});
} catch (err) {
console.error(err);
dispatch({
type: 'HIDE_VIDEO_ONBOARDING',
});
}
};
getData();
}, []);
// Hide the player in case of request error
if (isLoading) {
return null;
}
componentDidUpdate(prevProps) {
const { shouldOpenModal } = this.props;
if (shouldOpenModal !== prevProps.shouldOpenModal && shouldOpenModal) {
this.handleOpenModal();
}
}
componentWillUnmount() {
this.props.removeVideos();
}
setVideoEnd = () => {
this.setVideoEnd();
};
didPlayVideo = (index, currTime) => {
const eventName = `didPlay${index}GetStartedVideo`;
this.context.emitEvent(eventName, { timestamp: currTime });
};
didStopVideo = (index, currTime) => {
const eventName = `didStop${index}Video`;
this.context.emitEvent(eventName, { timestamp: currTime });
};
handleOpenModal = () => this.setState({ showVideos: true });
handleVideosToggle = () => {
this.setState(prevState => ({ showVideos: !prevState.showVideos }));
const { showVideos } = this.state;
const eventName = showVideos
const handleClick = () => {
const eventName = isOpen
? 'didOpenGetStartedVideoContainer'
: 'didCloseGetStartedVideoContainer';
this.context.emitEvent(eventName);
dispatch({ type: 'SET_IS_OPEN' });
emitEvent(eventName);
};
const handleClickOpenVideo = videoIndexToOpen => {
dispatch({
type: 'TOGGLE_VIDEO_MODAL',
videoIndexToOpen,
});
};
const handleUpdateVideoStartTime = (videoIndex, elapsedTime) => {
dispatch({
type: 'UPDATE_VIDEO_STARTED_TIME_AND_PLAYED_INFOS',
videoIndex,
elapsedTime,
});
};
const setVideoDuration = (videoIndex, duration) => {
dispatch({
type: 'SET_VIDEO_DURATION',
duration,
videoIndex,
});
};
updateCurrentTime = (index, current, duration) => {
this.props.updateVideoStartTime(index, current);
const hasVideos = videos.length > 0;
const className = hasVideos ? 'visible' : 'hidden';
const percent = (current * 100) / duration;
const video = this.props.videos[index];
if (percent >= 80) {
if (video.end === false) {
this.updateEnd(index);
}
}
};
updateEnd = index => {
this.props.setVideoEnd(index, true);
};
static contextType = GlobalContext;
render() {
const { videos, onClick, setVideoDuration } = this.props;
const { showVideos } = this.state;
const style = showVideos ? {} : { maxWidth: 0 };
return (
<Wrapper
style={style}
className={cn(videos.length > 0 ? 'visible' : 'hidden')}
>
<div
style={style}
className={cn(
'videosContent',
this.state.showVideos ? 'shown' : 'hide'
)}
>
<div className="videosHeader">
<p>
<FormattedMessage id="app.components.Onboarding.title" />
</p>
{videos.length && (
<p>
{Math.floor(
(videos.filter(v => v.end).length * 100) / videos.length
)}
<FormattedMessage id="app.components.Onboarding.label.completed" />
</p>
return (
<Wrapper className={className} isOpen={isOpen}>
<div className={cn('videosContent', isOpen ? 'shown' : 'hide')}>
<div className="videosHeader">
<p>
<FormattedMessage id="app.components.Onboarding.title" />
</p>
<p>
{Math.floor(
(videos.filter(v => v.end).length * 100) / videos.length
)}
</div>
<ul className="onboardingList">
{videos.map((video, i) => {
return (
<OnboardingVideo
key={i}
id={i}
video={video}
onClick={onClick}
setVideoDuration={setVideoDuration}
getVideoCurrentTime={this.updateCurrentTime}
didPlayVideo={this.didPlayVideo}
didStopVideo={this.didStopVideo}
/>
);
})}
</ul>
<FormattedMessage id="app.components.Onboarding.label.completed" />
</p>
</div>
<ul className="onboardingList">
{videos.map((video, index) => {
return (
<Video
key={video.id || index}
id={index}
video={video}
onClick={() => handleClickOpenVideo(index)}
setVideoDuration={(_, duration) => {
setVideoDuration(index, duration);
}}
getVideoCurrentTime={(_, elapsedTime) => {
handleUpdateVideoStartTime(index, elapsedTime);
}}
didPlayVideo={(_, elapsedTime) => {
const eventName = `didPlay${index}GetStartedVideo`;
<div className="openBtn">
<button
onClick={this.handleVideosToggle}
className={this.state.showVideos ? 'active' : ''}
type="button"
>
<i className="fa fa-question" />
<i className="fa fa-times" />
<span />
</button>
</div>
</Wrapper>
);
}
}
emitEvent(eventName, { timestamp: elapsedTime });
}}
didStopVideo={(_, elapsedTime) => {
const eventName = `didStop${index}Video`;
Onboarding.defaultProps = {
onClick: () => {},
removeVideos: () => {},
setVideoDuration: () => {},
setVideoEnd: () => {},
shouldOpenModal: false,
videos: [],
updateVideoStartTime: () => {},
};
Onboarding.propTypes = {
getVideos: PropTypes.func.isRequired,
onClick: PropTypes.func,
removeVideos: PropTypes.func,
setVideoDuration: PropTypes.func,
setVideoEnd: PropTypes.func,
shouldOpenModal: PropTypes.bool,
updateVideoStartTime: PropTypes.func,
videos: PropTypes.array,
};
const mapStateToProps = makeSelectOnboarding();
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
getVideos,
onClick,
setVideoDuration,
updateVideoStartTime,
setVideoEnd,
removeVideos,
},
dispatch
emitEvent(eventName, { timestamp: elapsedTime });
}}
/>
);
})}
</ul>
</div>
<div className="openBtn">
<button
onClick={handleClick}
className={isOpen ? 'active' : ''}
type="button"
>
<i className="fa fa-question" />
<i className="fa fa-times" />
<span />
</button>
</div>
</Wrapper>
);
}
};
const withConnect = connect(
mapStateToProps,
mapDispatchToProps
);
/* Remove this line if the container doesn't have a route and
* check the documentation to see how to create the container's store
*/
const withReducer = injectReducer({ key: 'onboarding', reducer });
/* Remove the line below the container doesn't have a route and
* check the documentation to see how to create the container's store
*/
const withSaga = injectSaga({ key: 'onboarding', saga });
export default compose(
withReducer,
withSaga,
withConnect
)(Onboarding);
export default memo(OnboardingVideos);

View File

@ -1,78 +1,67 @@
/*
*
* Onboarding reducer
*
*/
import { fromJS } from 'immutable';
import {
GET_VIDEOS_SUCCEEDED,
SHOULD_OPEN_MODAL,
ON_CLICK,
SET_VIDEOS_DURATION,
UPDATE_VIDEO_START_TIME,
SET_VIDEO_END,
REMOVE_VIDEOS,
} from './constants';
const initialState = fromJS({
videos: fromJS([]),
isLoading: true,
isOpen: false,
videos: [],
});
function onboardingReducer(state = initialState, action) {
const reducer = (state, action) => {
switch (action.type) {
case GET_VIDEOS_SUCCEEDED:
return state.update('videos', () => fromJS(action.videos));
case SHOULD_OPEN_MODAL:
return state.update('shouldOpenModal', () => action.opened);
case ON_CLICK:
return state.updateIn(['videos'], list => {
return list.reduce((acc, current, index) => {
if (index === action.index) {
return acc.updateIn([index, 'isOpen'], v => !v);
case 'GET_DATA_SUCCEEDED':
return state
.update('isOpen', () => !action.didWatchVideos)
.update('isLoading', () => false)
.update('videos', () => fromJS(action.videos));
case 'SET_IS_OPEN':
return state.update('isOpen', v => !v);
case 'SET_VIDEO_DURATION':
return state.updateIn(['videos', action.videoIndex, 'duration'], () => {
return parseFloat(action.duration, 10);
});
case 'TOGGLE_VIDEO_MODAL':
return state.update('videos', list => {
return list.map((item, index) => {
if (index === action.videoIndexToOpen) {
return item.update('isOpen', v => !v);
}
return acc.updateIn([index, 'isOpen'], () => false);
}, list);
return item.set('isOpen', false);
});
});
case SET_VIDEOS_DURATION:
return state.updateIn(
['videos', action.index, 'duration'],
() => action.duration
case 'UPDATE_VIDEO_STARTED_TIME_AND_PLAYED_INFOS': {
const updatedState = state.updateIn(
['videos', action.videoIndex],
video => {
const elapsedTime = parseFloat(action.elapsedTime, 10);
const videoDuration = parseFloat(video.get('duration', 10));
const percentElapsedTime = (elapsedTime * 100) / videoDuration;
return video
.update('startTime', () => elapsedTime)
.update('end', oldValue => {
if (oldValue === true) {
return true;
}
return percentElapsedTime > 80;
});
}
);
case UPDATE_VIDEO_START_TIME: {
const storedVideos = JSON.parse(localStorage.getItem('videos'));
const videos = state.updateIn(['videos'], list => {
return list.reduce((acc, current, index) => {
if (index === action.index) {
storedVideos[index].startTime = action.startTime;
return acc.updateIn([index, 'startTime'], () => action.startTime);
}
const videos = updatedState
.get('videos')
.map(video => video.set('isOpen', false));
storedVideos[index].startTime = 0;
// Update the local storage
localStorage.setItem('videos', JSON.stringify(videos.toJS()));
return acc.updateIn([index, 'startTime'], () => 0);
}, list);
});
localStorage.setItem('videos', JSON.stringify(storedVideos));
return videos;
return updatedState;
}
case SET_VIDEO_END: {
const storedVideos = JSON.parse(localStorage.getItem('videos'));
storedVideos[action.index].end = action.end;
localStorage.setItem('videos', JSON.stringify(storedVideos));
return state.updateIn(['videos', action.index, 'end'], () => action.end);
}
case REMOVE_VIDEOS:
return initialState;
default:
return state;
}
}
};
export default onboardingReducer;
export default reducer;
export { initialState };

View File

@ -1,72 +0,0 @@
/* eslint-disable no-console */
import { request } from 'strapi-helper-plugin';
import { all, call, fork, takeLatest, put } from 'redux-saga/effects';
import { GET_VIDEOS } from './constants';
import { getVideosSucceeded, shouldOpenModal } from './actions';
function* getVideos() {
try {
const data = yield call(
request,
'https://strapi.io/videos',
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
},
false,
true,
{ noAuth: true }
);
const storedVideo = JSON.parse(localStorage.getItem('videos')) || null;
const videos = data
.map(video => {
const { end, startTime } = storedVideo
? storedVideo.find(v => v.order === video.order)
: { end: false, startTime: 0 };
return {
...video,
duration: null,
end,
isOpen: false,
key: video.order,
startTime,
};
})
.sort((a, b) => a.order - b.order);
localStorage.setItem('videos', JSON.stringify(videos));
yield put(getVideosSucceeded(videos));
const isFirstTime = JSON.parse(localStorage.getItem('onboarding')) || null;
if (isFirstTime === null) {
yield new Promise(resolve => {
setTimeout(() => {
resolve();
}, 500);
});
yield put(shouldOpenModal(true));
localStorage.setItem('onboarding', true);
}
} catch (err) {
console.log(err); // eslint-disable-line no-console
}
}
function* defaultSaga() {
try {
yield all([fork(takeLatest, GET_VIDEOS, getVideos)]);
} catch (err) {
console.log(err);
}
}
export default defaultSaga;

View File

@ -1,25 +0,0 @@
import { createSelector } from 'reselect';
/**
* Direct selector to the onboarding state domain
*/
const selectOnboardingDomain = () => (state) => state.get('onboarding');
/**
* Other specific selectors
*/
/**
* Default selector used by Onboarding
*/
const makeSelectOnboarding = () => createSelector(
selectOnboardingDomain(),
(substate) => substate.toJS()
);
export default makeSelectOnboarding;
export {
selectOnboardingDomain,
};

View File

@ -1,6 +1,6 @@
const formatVideosArray = array => {
const alreadyFetchedVideos = JSON.parse(localStorage.getItem('videos')) || [];
const didWatchVideos = alreadyFetchedVideos.length !== array.length;
const didWatchVideos = alreadyFetchedVideos.length === array.length;
let videos;
if (!didWatchVideos) {

View File

@ -1,127 +0,0 @@
import styled, { keyframes } from 'styled-components';
const fadeIn = keyframes`
0% {
width: auto;
height: auto;
opacity: 0;
}
5% {
opacity: 0;
}
100% {
opacity: 1;
}
`;
const fadeOut = keyframes`
0% {
opacity: 1;
}
60% {
opacity: 0;
}
100% {
opacity: 0;
width: 0;
height: 0;
}
`;
const Wrapper = styled.div`
position: fixed;
right: 15px;
bottom: 15px;
button,
button:focus,
a {
cursor: pointer;
outline: 0;
}
p {
margin-bottom: 0;
}
.videosHeader {
padding: 25px 15px 0 15px;
p {
display: inline-block;
vertical-align: top;
width: 50%;
font-family: Lato;
font-weight: bold;
font-size: 11px;
color: #5c5f66;
letter-spacing: 0.5px;
text-transform: uppercase;
&:last-of-type {
color: #5a9e06;
text-align: right;
}
}
}
&.visible {
opacity: 1;
z-index: 10;
}
&.hidden {
opacity: 0;
}
.videosContent {
min-width: 320px;
margin-bottom: 10px;
margin-right: 15px;
background-color: white;
box-shadow: 0 2px 4px 0 #e3e9f3;
border-radius: 3px;
overflow: hidden;
&.shown {
animation: ${fadeIn} 0.5s forwards;
}
&.hide {
min-width: 0;
animation: ${fadeOut} 0.5s forwards;
}
ul {
padding: 0 0 10px 0;
margin-bottom: 0;
list-style: none;
}
}
.openBtn {
float: right;
width: 38px;
height: 38px;
button {
width: 100%;
height: 100%;
border-radius: 50%;
color: white;
background: #0e7de7;
box-shadow: 0px 2px 4px 0px rgba(227, 233, 243, 1);
i,
svg {
margin: auto;
}
i:last-of-type,
svg:last-of-type {
display: none;
}
&.active {
i:first-of-type,
svg:first-of-type {
display: none;
}
i:last-of-type,
svg:last-of-type {
display: block;
}
}
}
}
`;
export default Wrapper;

View File

@ -1,44 +0,0 @@
import React, { useEffect, useReducer } from 'react';
import axios from 'axios';
import Wrapper from './Wrapper';
import formatVideoArray from './utils/formatAndStoreVideoArray';
import init from './init';
import reducer, { initialState } from './reducer';
const OnboardingVideos = () => {
const [reducerState, dispatch] = useReducer(reducer, initialState, init);
const { shouldShowVideoOnboarding } = reducerState.toJS();
useEffect(() => {
const getData = async () => {
try {
const { data } = await axios.get('https://strapi.io/videos', {
timeout: 1000,
});
const { didWatchVideos, videos } = formatVideoArray(data);
dispatch({
type: 'GET_DATA_SUCCEEDED',
didWatchVideos,
videos,
});
} catch (err) {
console.error(err);
dispatch({
type: 'HIDE_VIDEO_ONBOARDING',
});
}
};
getData();
}, []);
// Hide the player in case of request error
if (!shouldShowVideoOnboarding) {
return null;
}
return <Wrapper>COMING SOON</Wrapper>;
};
export default OnboardingVideos;

View File

@ -1,19 +0,0 @@
import { fromJS } from 'immutable';
const initialState = fromJS({
isOpened: false,
shouldShowVideoOnboarding: true,
videos: [],
});
const reducer = (state, action) => {
switch (action.type) {
case 'HIDE_VIDEO_ONBOARDING':
return state.update('shouldShowVideoOnboarding', () => false);
default:
return state;
}
};
export default reducer;
export { initialState };