diff --git a/app/javascript/src/components/AlertsContainer/components/Alert.jsx b/app/javascript/src/components/AlertsContainer/components/Alert.jsx new file mode 100644 index 000000000..e664093a0 --- /dev/null +++ b/app/javascript/src/components/AlertsContainer/components/Alert.jsx @@ -0,0 +1,81 @@ +import React, { Component } from "react"; +import styled from "styled-components"; +import { string, number, func } from "prop-types"; +import { Grid, Row, Col } from "react-bootstrap"; + +const Wrapper = styled.div` + margin-bottom: 0; +`; + +class Alert extends Component { + static alertClass(type) { + const classes = { + error: "alert-danger", + alert: "alert-warning", + notice: "alert-info", + success: "alert-success" + }; + return classes[type] || classes.success; + } + + static glyphiconClass(type) { + const classes = { + error: "glyphicon-exclamation-sign", + alert: "glyphicon-exclamation-sign", + notice: "glyphicon-info-sign", + success: "glyphicon-ok-sign" + }; + return classes[type] || classes.success; + } + + componentDidMount() { + this.timer = setTimeout( + this.props.onClose, + this.props.timeout + ); + } + + componentWillUnmount() { + clearTimeout(this.timer); + } + + render() { + const alertClassName = + `alert + ${Alert.alertClass(this.props.type)} + alert-dismissable + alert-floating`; + const glyphiconClassName = + `glyphicon + ${Alert.glyphiconClass(this.props.type)}`; + + return ( + + + + + + +  {this.props.message} + + + + + ); + } +} + +Alert.propTypes = { + message: string.isRequired, + type: string.isRequired, + timeout: number.isRequired, + onClose: func.isRequired +}; + +export default Alert; \ No newline at end of file diff --git a/app/javascript/src/components/AlertsContainer/index.jsx b/app/javascript/src/components/AlertsContainer/index.jsx new file mode 100644 index 000000000..0577b1ad0 --- /dev/null +++ b/app/javascript/src/components/AlertsContainer/index.jsx @@ -0,0 +1,65 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import styled from "styled-components"; +import TransitionGroup from 'react-transition-group/TransitionGroup'; +import CSSTransition from 'react-transition-group/CSSTransition'; +import { shape, arrayOf, string, number, func } from "prop-types"; +import { clearAlert } from "../actions/AlertsActions"; +import Alert from "./components/Alert"; + +const Wrapper = styled.div` + position: absolute; + z-index: 1000; + width: 100%; +`; + +class AlertsContainer extends Component { + constructor(props) { + super(props); + + this.renderAlert = this.renderAlert.bind(this); + } + + renderAlert(alert) { + return ( + this.props.clearAlert(alert.id)} + /> + ); + } + + render() { + return ( + + + {this.props.alerts.map((alert) => + + {this.renderAlert(alert)} + + )} + + + ); + } +} + +AlertsContainer.propTypes = { + alerts: arrayOf( + shape({ + message: string.isRequired, + type: string.isRequired, + id: string.isRequired, + timeout: number, + onClose: func + }).isRequired + ).isRequired, + clearAlert: func.isRequired +} + +const mapStateToProps = ({ alerts }) => ({ alerts }); + +export default connect(mapStateToProps, { clearAlert })(AlertsContainer); \ No newline at end of file diff --git a/app/javascript/src/components/Navigation/index.jsx b/app/javascript/src/components/Navigation/index.jsx index 6fe32f058..5cc2c5e9a 100644 --- a/app/javascript/src/components/Navigation/index.jsx +++ b/app/javascript/src/components/Navigation/index.jsx @@ -20,6 +20,7 @@ import UserAccountDropdown from "./components/UserAccountDropdown"; const StyledNavbar = styled(Navbar)` background-color: ${WHITE_COLOR}; border-color: ${BORDER_GRAY_COLOR}; + margin-bottom: 0; `; const StyledBrand = styled.a` diff --git a/app/javascript/src/components/actions/AlertsActions.js b/app/javascript/src/components/actions/AlertsActions.js new file mode 100644 index 000000000..f6ff5b175 --- /dev/null +++ b/app/javascript/src/components/actions/AlertsActions.js @@ -0,0 +1,29 @@ +import shortid from "shortid"; +import { + ADD_ALERT, + CLEAR_ALERT, + CLEAR_ALL_ALERTS +} from "../../config/action_types"; + +export function addAlert(message, + type, + id = shortid.generate(), + timeout = 5000) { + return { + payload: { + message, + type, + id, + timeout + }, + type: ADD_ALERT + }; +} + +export function clearAlert(id) { + return { payload: id, type: CLEAR_ALERT } +} + +export function clearAllAlerts() { + return { type: CLEAR_ALL_ALERTS }; +} \ No newline at end of file diff --git a/app/javascript/src/components/reducers/AlertsReducers.js b/app/javascript/src/components/reducers/AlertsReducers.js new file mode 100644 index 000000000..098459ca6 --- /dev/null +++ b/app/javascript/src/components/reducers/AlertsReducers.js @@ -0,0 +1,31 @@ +import { + ADD_ALERT, + CLEAR_ALERT, + CLEAR_ALL_ALERTS +} from "../../config/action_types"; + +export const alerts = ( + state = [], + action +) => { + switch(action.type) { + case ADD_ALERT: + return [ + ...state, + { + message: action.payload.message, + type: action.payload.type, + id: action.payload.id, + timeout: action.payload.timeout + } + ]; + case CLEAR_ALERT: + return state.filter((alert) => ( + alert.id !== action.payload + )); + case CLEAR_ALL_ALERTS: + return []; + default: + return state; + } +}; \ No newline at end of file diff --git a/app/javascript/src/config/action_types.js b/app/javascript/src/config/action_types.js index b2d3a7443..732a81db4 100644 --- a/app/javascript/src/config/action_types.js +++ b/app/javascript/src/config/action_types.js @@ -35,3 +35,8 @@ export const UPDATE_TEAM_DESCRIPTION_MODAL = "UPDATE_TEAM_DESCRIPTION_MODAL"; // spinner export const SPINNER_ON = "SPINNER_ON"; export const SPINNER_OFF = "SPINNER_OFF"; + +// alerts +export const ADD_ALERT = "ADD_ALERT"; +export const CLEAR_ALERT = "CLEAR_ALERT"; +export const CLEAR_ALL_ALERTS = "CLEAR_ALL_ALERTS"; \ No newline at end of file diff --git a/app/javascript/src/config/reducers.js b/app/javascript/src/config/reducers.js index 2f00797ea..5561d4cb3 100644 --- a/app/javascript/src/config/reducers.js +++ b/app/javascript/src/config/reducers.js @@ -6,11 +6,13 @@ import { } from "../components/reducers/TeamReducers"; import { globalActivities } from "../components/reducers/ActivitiesReducers"; import { currentUser } from "../components/reducers/UsersReducer"; +import { alerts } from "../components/reducers/AlertsReducers"; export default combineReducers({ current_team: setCurrentTeam, all_teams: getListOfTeams, global_activities: globalActivities, current_user: currentUser, - showLeaveTeamModal + showLeaveTeamModal, + alerts }); diff --git a/app/javascript/src/index.jsx b/app/javascript/src/index.jsx index 23fbf1cb5..1328b9fca 100644 --- a/app/javascript/src/index.jsx +++ b/app/javascript/src/index.jsx @@ -3,11 +3,13 @@ import { BrowserRouter } from "react-router-dom"; import { Provider } from "react-redux"; import { IntlProvider, addLocaleData } from "react-intl"; import enLocaleData from "react-intl/locale-data/en"; +import styled from "styled-components"; import { flattenMessages } from "./config/locales/utils"; import messages from "./config/locales/messages"; import store from "./config/store"; import Spinner from "./components/Spinner"; +import AlertsContainer from "./components/AlertsContainer"; import ModalsContainer from "./components/ModalsContainer"; import SettingsPage from "./scenes/SettingsPage"; import Navigation from "./components/Navigation"; @@ -15,14 +17,22 @@ import Navigation from "./components/Navigation"; addLocaleData([...enLocaleData]); const locale = "en-US"; -export default () => +const ContentWrapper = styled.div` + margin-top: 15px; +`; + +const ScinoteApp = () => - +
- + + + +
@@ -31,3 +41,5 @@ export default () =>
; + +export default ScinoteApp; diff --git a/app/javascript/src/styles/animations.scss b/app/javascript/src/styles/animations.scss new file mode 100644 index 000000000..32a90a5a4 --- /dev/null +++ b/app/javascript/src/styles/animations.scss @@ -0,0 +1,30 @@ +.alert-animated-enter { + opacity: .01; + + &.alert-animated-enter-active { + opacity: 1; + transition: opacity 150ms ease-in; + } +} + +.alert-animated-exit { + opacity: 1; + padding-bottom: 15px; + padding-top: 15px; + margin-bottom: 20px; + height: 50px; + + &.alert-animated-exit-active { + overflow: hidden; + opacity: .01; + padding-top: 0; + padding-bottom: 0; + margin-bottom: 0; + height: 0; + transition: opacity 300ms ease-in, + padding-top 500ms ease-in, + padding-bottom 500ms ease-in, + margin-bottom 500ms ease-in, + height 500ms ease-in; + } +} diff --git a/app/javascript/src/styles/main.scss b/app/javascript/src/styles/main.scss index 434b3392b..005b68e10 100644 --- a/app/javascript/src/styles/main.scss +++ b/app/javascript/src/styles/main.scss @@ -1,4 +1,5 @@ @import 'constants'; +@import 'animations'; @import 'react-bootstrap-timezone-picker/dist/react-bootstrap-timezone-picker.min.css'; @import '~react-bootstrap-table/dist/react-bootstrap-table.min'; @import 'react-tagsinput/react-tagsinput.css'; diff --git a/package.json b/package.json index dd89a39d0..651b0ceb2 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "react-bootstrap-timezone-picker": "^1.0.11", "react-data-grid": "^2.0.2", "react-tagsinput": "^3.17.0", + "react-transition-group": "^2.2.0", "react-dom": "^15.6.1", "react-intl": "^2.3.0", "react-intl-redux": "^0.6.0", @@ -82,6 +83,7 @@ "redux-thunk": "^2.2.0", "resolve-url-loader": "^2.1.0", "sass-loader": "^6.0.6", + "shortid": "^2.2.8", "style-loader": "^0.18.2", "styled-components": "^2.1.1", "webpack": "^3.2.0", diff --git a/yarn.lock b/yarn.lock index 61840a4cc..062e042d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1156,6 +1156,10 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" +chain-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc" + chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -4825,6 +4829,17 @@ react-timezone@^0.2.0: dependencies: classnames "^2.2.1" +react-transition-group@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.2.0.tgz#793bf8cb15bfe91b3101b24bce1c1d2891659575" + dependencies: + chain-function "^1.0.0" + classnames "^2.2.5" + dom-helpers "^3.2.0" + loose-envify "^1.3.1" + prop-types "^15.5.8" + warning "^3.0.0" + react@^15.6.1: version "15.6.1" resolved "https://registry.yarnpkg.com/react/-/react-15.6.1.tgz#baa8434ec6780bde997cdc380b79cd33b96393df" @@ -5298,6 +5313,10 @@ shelljs@^0.7.5: interpret "^1.0.0" rechoir "^0.6.2" +shortid@^2.2.8: + version "2.2.8" + resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.8.tgz#033b117d6a2e975804f6f0969dbe7d3d0b355131" + signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"