Skip to content
Permalink
Browse files

- add Notifications System

  • Loading branch information...
Vladimir Upirov
Vladimir Upirov committed Nov 6, 2019
1 parent 0691080 commit ea8062d6a3ed0186ca020660f81be5310f8511ca
@@ -9,6 +9,7 @@
"react-dom": "^16.8.4",
"react-redux": "^6.0.1",
"react-scripts": "2.1.8",
"react-transition-group": "^4.3.0",
"redux": "^4.0.1",
"redux-thunk": "^2.3.0"
},
@@ -0,0 +1,64 @@
import React from 'react'
import { CSSTransition } from 'react-transition-group'

export default class AlertView extends React.Component {

componentDidMount() {
this.startTimer()
}

componentWillUnmount() {
this.stopTimer()
}

close = () => {
this.props.onClose(this.props.alert)
}

stopTimer = () => {
if (this.closeTimer) {
clearTimeout(this.closeTimer)
}
}

startTimer = () => {
this.stopTimer()

const closeTimeout = this.props.alert.timeout

if (closeTimeout) {
this.closeTimer = setTimeout(this.close, closeTimeout)
}
}

render() {
const { alert, ...props } = this.props

return (
<CSSTransition
classNames={{
enter : 'transition-enter',
enterActive: 'transition-enter-active',
exit : 'transition-exit',
exitActive : 'transition-exit-active',
}}
onMouseLeave={this.startTimer}
onMouseEnter={this.stopTimer}
timeout={{ enter: 300, exit: 500 }}
{...props}>

<div className="alert alert-primary">
<button className="close" data-dismiss="alert" aria-label="Close" onClick={this.close}>
<span aria-hidden="true">&times;</span>
</button>

<div className="alert-message">
{alert.message}
</div>
</div>
</CSSTransition>
)
}

}

@@ -0,0 +1,37 @@
import React, { Component } from 'react'
import { TransitionGroup } from 'react-transition-group'

import AlertsManager from './manager'
import AlertView from './alert'

export default class AlertsContainer extends Component {

state = {
items: [],
}

componentDidMount() {
AlertsManager.subscribe(this.onAlertsChange)
}

onAlertsChange = items => {
this.setState({ items })
}

onItemClose = alert => {
AlertsManager.remove(alert._id)
}

render() {
const { items } = this.state

return (
<TransitionGroup className="alerts-container">
{items.map(alert => (
<AlertView key={alert._id} alert={alert} onClose={this.onItemClose}/>
))}
</TransitionGroup>
)
}

}
@@ -0,0 +1,11 @@
import './style.css';

import AlertsManager from './manager'
import AlertsContainer from './container'

export default AlertsContainer

export const addAlert = (message, timeout) => AlertsManager.add({
timeout,
message,
});
@@ -0,0 +1,44 @@
const ACTIVE_ITEMS_LIMIT = 1

const chr8 = () => Math.random().toString(16).slice(-8)

const AlertsManager = {

handler: null,

allItems : [],
activeItems: [],

add({ message, timeout }) {
const item = {
_id : `${chr8()}-${chr8()}`,
timeout: timeout || 5 * 1000,
message,
}

this.update([...this.allItems, item])

return item
},

remove(alertId) {
this.update(this.allItems.filter(item => item._id !== alertId))
},

update(allItems) {
this.allItems = allItems
this.activeItems = allItems.slice(0, ACTIVE_ITEMS_LIMIT)

if (this.handler) {
this.handler(this.activeItems)
}
},

subscribe(handler) {
this.handler = handler
this.handler(this.activeItems)
},

}

export default AlertsManager
@@ -0,0 +1,40 @@
.alerts-container {
position: fixed;
right: 0;
top: 0;
padding: 20px;
width: 350px;
max-height: 100%;
z-index: 999999;
overflow: auto;
}

.alerts-container .alert {
position: relative;
overflow: hidden;
}

.alerts-container .alert .close {
position: absolute;
top: 3px;
right: 5px;
outline: none;
}

.alerts-container .alert.transition-enter {
transform: scale(0);
}

.alerts-container .alert.transition-enter-active {
transform: scale(1);
transition: all 300ms cubic-bezier(0, 0, 0.5, 1.5);
}

.alerts-container .alert.transition-exit {
transform: scale(1);
}

.alerts-container .alert.transition-exit-active {
transform: scale(0);
transition: all 300ms ease-in-out;
}
@@ -1,6 +1,7 @@
import React from 'react';

import Persons from './persons';
import Alerts from './alerts';

export default function App() {
return (
@@ -12,6 +13,8 @@ export default function App() {
</div>

<Persons/>

<Alerts/>
</div>
);
}
}
@@ -4,6 +4,7 @@ import Button from 'react-bootstrap/Button';
import ButtonGroup from 'react-bootstrap/ButtonGroup';
import Backendless from 'backendless';

import { addAlert } from '../alerts'
import { loadPersons, getPersons, onPersonCreate, onPersonUpdate, onPersonRemove } from '../store';

import Editor from './editor';
@@ -41,15 +42,30 @@ class Persons extends Component {

this.personRT = Backendless.Data.of('Person').rt();

this.personRT.addCreateListener(this.props.onPersonCreate);
this.personRT.addUpdateListener(this.props.onPersonUpdate);
this.personRT.addDeleteListener(this.props.onPersonRemove);
this.personRT.addCreateListener(this.onPersonCreate);
this.personRT.addUpdateListener(this.onPersonUpdate);
this.personRT.addDeleteListener(this.onPersonRemove);
}

componentWillUnmount(){
this.personRT.removeCreateListener(this.props.onPersonCreate);
this.personRT.removeUpdateListener(this.props.onPersonUpdate);
this.personRT.removeDeleteListener(this.props.onPersonRemove);
this.personRT.removeCreateListener(this.onPersonCreate);
this.personRT.removeUpdateListener(this.onPersonUpdate);
this.personRT.removeDeleteListener(this.onPersonRemove);
}

onPersonCreate = person => this.props.onPersonCreate(person)
onPersonRemove = person => this.props.onPersonRemove(person)

onPersonUpdate = person => {
const { persons } = this.props

const personInStore = persons.find(p => p.objectId === person.objectId)

if (personInStore && person.name !== personInStore.name) {
addAlert(`Person with name "${personInStore.name}" has changed his name to "${person.name}"!`)
}

this.props.onPersonUpdate(person)
}

onAddClick = () => this.showEditor(null);
@@ -4,7 +4,11 @@ export default mirrorKeys({
LOAD_PERSONS_FAIL : null,

CREATE_PERSON_SUCCESS: null,

UPDATE_PERSON : null,
UPDATE_PERSON_SUCCESS: null,
UPDATE_PERSON_FAIL : null,

REMOVE_PERSON_SUCCESS: null,

ON_PERSON_CREATE: null,
@@ -1,6 +1,7 @@
import Backendless from 'backendless'

import t from '../action-types';
import { getPersons } from '../reducers';

export const loadPersons = () => ({
types : [t.LOAD_PERSONS, t.LOAD_PERSONS_SUCCESS, t.LOAD_PERSONS_FAIL],
@@ -12,10 +13,17 @@ export const createPerson = person => ({
apiCall: () => Backendless.Data.of('Person').save(person),
});

export const updatePerson = person => ({
types : [null, t.UPDATE_PERSON_SUCCESS, null],
apiCall: () => Backendless.Data.of('Person').save(person),
});
export const updatePerson = person => (dispatch, getState) => {
const persons = getPersons(getState()).list
const prevPerson = persons.find(p => p.objectId === person.objectId)

return dispatch({
person,
prevPerson,
types : [t.UPDATE_PERSON, t.UPDATE_PERSON_SUCCESS, t.UPDATE_PERSON_FAIL],
apiCall: () => Backendless.Data.of('Person').save(person),
})
};

export const removePerson = personId => ({
personId,
@@ -13,7 +13,11 @@ const personsReducer = reduceReducers(initialState,

reducersMap({
[t.CREATE_PERSON_SUCCESS]: (state, action) => addPerson(state, action.result),

[t.UPDATE_PERSON] : (state, action) => updatePerson(state, action.person),
[t.UPDATE_PERSON_SUCCESS]: (state, action) => updatePerson(state, action.result),
[t.UPDATE_PERSON_FAIL] : (state, action) => updatePerson(state, action.prevPerson),

[t.REMOVE_PERSON_SUCCESS]: (state, action) => deletePerson(state, action.personId),

[t.ON_PERSON_CREATE]: (state, action) => addPerson(state, action.person),
@@ -47,4 +51,4 @@ function deletePerson(state, personId) {
}
}

export default personsReducer
export default personsReducer

0 comments on commit ea8062d

Please sign in to comment.
You can’t perform that action at this time.