diff --git a/README.md b/README.md index ade4659e..47c957fe 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,5 @@ Note that on the FTP and sFTP, sessions connections aren't destroyed on every re Nuage is an open source software with its source code available under the AGPL license. Commercial license and support is available upon request, contact me for details: mickael@kerjean.me # Credits -- Icons from www.flaticon.com -- Folks developing awesome [libraries](https://github.com/mickael-kerjean/nuage/blob/master/package.json) as Nuage is just butter and cream on top. +- Iconography: www.flaticon.com, fontawesome.com and material.io +- Folks developing awesome [libraries](https://github.com/mickael-kerjean/nuage/blob/master/package.json) diff --git a/client/assets/img/arrow-down-double.svg b/client/assets/img/arrow-down-double.svg new file mode 100644 index 00000000..9351e57d --- /dev/null +++ b/client/assets/img/arrow-down-double.svg @@ -0,0 +1,90 @@ + + diff --git a/client/assets/img/arrow-down.svg b/client/assets/img/arrow-down.svg new file mode 100644 index 00000000..3504d73f --- /dev/null +++ b/client/assets/img/arrow-down.svg @@ -0,0 +1,90 @@ + + diff --git a/client/assets/img/arrow-up-double.svg b/client/assets/img/arrow-up-double.svg new file mode 100644 index 00000000..6aa6b854 --- /dev/null +++ b/client/assets/img/arrow-up-double.svg @@ -0,0 +1,90 @@ + + diff --git a/client/assets/img/arrow_right.svg b/client/assets/img/arrow_right.svg new file mode 100644 index 00000000..fca0fafb --- /dev/null +++ b/client/assets/img/arrow_right.svg @@ -0,0 +1,59 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/client/assets/img/calendar.svg b/client/assets/img/calendar.svg new file mode 100644 index 00000000..b9b6eeca --- /dev/null +++ b/client/assets/img/calendar.svg @@ -0,0 +1,55 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/client/assets/img/calendar_white.svg b/client/assets/img/calendar_white.svg new file mode 100644 index 00000000..c869463d --- /dev/null +++ b/client/assets/img/calendar_white.svg @@ -0,0 +1,55 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/client/assets/img/close.svg b/client/assets/img/close.svg new file mode 100644 index 00000000..7fc48d3e --- /dev/null +++ b/client/assets/img/close.svg @@ -0,0 +1,100 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/assets/img/deadline.svg b/client/assets/img/deadline.svg new file mode 100644 index 00000000..5184283e --- /dev/null +++ b/client/assets/img/deadline.svg @@ -0,0 +1,52 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/client/assets/img/download_white.svg b/client/assets/img/download_white.svg new file mode 100644 index 00000000..1c5218eb --- /dev/null +++ b/client/assets/img/download_white.svg @@ -0,0 +1,60 @@ + + diff --git a/client/assets/img/error.svg b/client/assets/img/error.svg index e5bcec14..9be16548 100644 --- a/client/assets/img/error.svg +++ b/client/assets/img/error.svg @@ -1,37 +1,98 @@ - + - - - + +image/svg+xml + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + \ No newline at end of file diff --git a/client/assets/img/more.svg b/client/assets/img/more.svg new file mode 100644 index 00000000..4219a16c --- /dev/null +++ b/client/assets/img/more.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/client/assets/img/schedule.svg b/client/assets/img/schedule.svg new file mode 100644 index 00000000..c14abb36 --- /dev/null +++ b/client/assets/img/schedule.svg @@ -0,0 +1,52 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/client/assets/img/todo_white.svg b/client/assets/img/todo_white.svg new file mode 100644 index 00000000..04942415 --- /dev/null +++ b/client/assets/img/todo_white.svg @@ -0,0 +1,55 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/client/components/dropdown.js b/client/components/dropdown.js new file mode 100644 index 00000000..b6e3306a --- /dev/null +++ b/client/components/dropdown.js @@ -0,0 +1,114 @@ +/* + * This component was build as an alternative to the select component. The idea is + * we replace the dirty select on desktop by something more fancy but not on ios/android + * as there's just no reason for doing that. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { Icon, NgIf } from "./"; +import { debounce } from "../helpers/"; +import './dropdown.scss'; + +export class Dropdown extends React.Component { + constructor(props){ + super(props); + this.state = { + button: false + }; + this.$dropdown = null; + this.closeDropdown = this.closeDropdown.bind(this); + this.toggleDropdown = this.toggleDropdown.bind(this); + } + + componentDidMount(){ + this.$dropdown = ReactDOM.findDOMNode(this).querySelector(".dropdown_button"); + // This is not really the "react" way of doing things but we needed to use both a + // click on the button and on the body (to exit the dropdown). we had issues + // that were impossible to solve the "react" way such as the dropdown button click + // event was triggered after the body click which makes it hard to cancel it ... + this.$dropdown.addEventListener("click", this.toggleDropdown); + } + + componentWillUnmount(){ + this.$dropdown.removeEventListener("click", this.toggleDropdown); + document.body.removeEventListener("click", this.closeDropdown); + } + + onSelect(name){ + this.props.onChange(name); + } + + closeDropdown(){ + document.body.removeEventListener("click", this.closeDropdown); + this.setState({button: false}); + } + + toggleDropdown(e){ + document.body.removeEventListener("click", this.closeDropdown); + this.setState({button: !this.state.button}, () => { + if(this.state.button === true){ + requestAnimationFrame(() => { + document.body.addEventListener("click", this.closeDropdown); + }); + } + }); + } + + render(){ + const button = this.props.children[0]; + if(button.type.name !== 'DropdownButton') throw("First children should be of type DropdownButton"); + + const dropdown = React.cloneElement(this.props.children[1], {onSelect: this.onSelect.bind(this)}); + if(dropdown.type.name !== 'DropdownList') throw("Second children should be of type DropdownList"); + + return ( +
+ { button } + { dropdown } +
+ ); + } +} + + +export const DropdownButton = (props) => { + return ( +
+ { props.children } +
+ ); +} + + +export const DropdownList = (props) => { + const childrens = Array.isArray(props.children) ? props.children : [props.children]; + return ( +
+ +
+ ); +}; + +export const DropdownItem = (props) => { + return ( +
+ {props.children} + + + + + +
+ ); +}; diff --git a/client/components/dropdown.scss b/client/components/dropdown.scss new file mode 100644 index 00000000..334e7dd4 --- /dev/null +++ b/client/components/dropdown.scss @@ -0,0 +1,49 @@ +.component_dropdown{ + position: relative; + .dropdown_container{display: none; position: absolute; right: 0;} + + .dropdown_button{ + border: 1px solid white; + border-radius: 4px; + padding: 5px; + min-width: 20px; + text-align: center; + } + .dropdown_container{ + padding-top: 3px; + z-index: 2; + ul{ + margin: 0; + list-style-type: none; + background: white; + border: 1px solid var(--bg-color); + box-shadow: 1px 1px 2px rgba(0,0,0,0.1); + color: var(--emphasis-secondary); + border-radius: 3px; + padding: 3px 0px; + font-size: 0.92em; + li{ + display: flex; + > div{ + width: 150px; + padding: 8px 5px 8px 10px; + } + } + } + } +} + +.component_dropdown{ + &.active{ + .dropdown_container{ + display: block; + li:hover{ + background: var(--bg-color); + } + } + .dropdown_button{ + border-color: var(--bg-color); + box-shadow: 1px 1px 2px rgba(0,0,0,0.1); + } + } +} diff --git a/client/components/icon.js b/client/components/icon.js index d42d117c..ad1789a9 100644 --- a/client/components/icon.js +++ b/client/components/icon.js @@ -11,11 +11,21 @@ import img_delete from "../assets/img/delete.svg"; import img_bucket from "../assets/img/bucket.svg"; import img_link from "../assets/img/link.svg"; import img_loading from "../assets/img/loader.svg"; -import img_download from "../assets/img/download.svg"; import img_play from "../assets/img/play.svg"; import img_pause from "../assets/img/pause.svg"; import img_error from "../assets/img/error.svg"; import img_loading_white from "../assets/img/loader_white.svg"; +import img_download_white from "../assets/img/download_white.svg"; +import img_todo_white from '../assets/img/todo_white.svg'; +import img_calendar_white from '../assets/img/calendar_white.svg'; +import img_arrow_right from '../assets/img/arrow_right.svg'; +import img_more from '../assets/img/more.svg'; +import img_close from '../assets/img/close.svg'; +import img_schedule from '../assets/img/schedule.svg'; +import img_deadline from '../assets/img/deadline.svg'; +import img_arrow_down from '../assets/img/arrow-down.svg'; +import img_arrow_up_double from '../assets/img/arrow-up-double.svg'; +import img_arrow_down_double from '../assets/img/arrow-down-double.svg'; export const Icon = (props) => { let img; @@ -39,8 +49,8 @@ export const Icon = (props) => { img = img_link; }else if(props.name === 'loading'){ img = img_loader; - }else if(props.name === 'download'){ - img = img_download; + }else if(props.name === 'download_white'){ + img = img_download_white; }else if(props.name === 'play'){ img = img_play; }else if(props.name === 'pause'){ @@ -49,6 +59,26 @@ export const Icon = (props) => { img = img_error; }else if(props.name === 'loading_white'){ img = img_loading_white; + }else if(props.name === 'calendar_white'){ + img = img_calendar_white; + }else if(props.name === 'schedule'){ + img = img_schedule; + }else if(props.name === 'deadline'){ + img = img_deadline; + }else if(props.name === 'todo_white'){ + img = img_todo_white; + }else if(props.name === 'arrow_right'){ + img = img_arrow_right; + }else if(props.name === 'more'){ + img = img_more; + }else if(props.name === 'close'){ + img = img_close; + }else if(props.name === 'arrow_up_double'){ + img = img_arrow_up_double; + }else if(props.name === 'arrow_down_double'){ + img = img_arrow_down_double; + }else if(props.name === 'arrow_down'){ + img = img_arrow_down; }else{ throw('unknown icon'); } @@ -60,4 +90,4 @@ export const Icon = (props) => { src={img} alt={props.name}/> ); -} +}; diff --git a/client/components/index.js b/client/components/index.js index 59c34c69..02fc182a 100644 --- a/client/components/index.js +++ b/client/components/index.js @@ -17,6 +17,7 @@ export { Notification } from './notification'; export { Alert } from './alert'; export { Audio } from './audio'; export { Video } from './video'; +export { Dropdown, DropdownButton, DropdownList, DropdownItem } from './dropdown'; //export { Connect } from './connect'; // Those are commented because they will delivered as a separate chunk // export { Editor } from './editor'; diff --git a/client/components/modal.js b/client/components/modal.js index 5293be68..090ea0a9 100644 --- a/client/components/modal.js +++ b/client/components/modal.js @@ -3,14 +3,17 @@ import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import PropTypes from 'prop-types'; import { Input, Button, NgIf } from './'; +import { debounce } from '../helpers/'; import './modal.scss'; export class Modal extends React.Component { constructor(props){ super(props); this.state = { - marginTop: this._marginTop() + marginTop: -1 }; + this._resetMargin = debounce(this._resetMargin.bind(this), 100); + this._onEscapeKeyPress = this._onEscapeKeyPress.bind(this); } onClick(e){ @@ -20,12 +23,26 @@ export class Modal extends React.Component { } componentDidMount(){ + this._resetMargin(); + window.addEventListener("resize", this._resetMargin); + window.addEventListener('keydown', this._onEscapeKeyPress); + } + componentWillUnmount(){ + window.removeEventListener("resize", this._resetMargin); + window.removeEventListener('keydown', this._onEscapeKeyPress); + } + + _resetMargin(){ this.setState({marginTop: this._marginTop()}); } - componentWillUnmount(){ + _onEscapeKeyPress(e){ + if(e.keyCode === 27){ + this.props.onQuit && this.props.onQuit(); + } } + _marginTop(){ let size = 300; const $box = document.querySelector('#modal-box > div'); @@ -45,8 +62,8 @@ export class Modal extends React.Component { transitionAppear={true} transitionAppearTimeout={300} > -