improvement (UI): make the UI more engaging and "modern"

This commit is contained in:
Mickael KERJEAN
2018-03-07 02:01:03 +11:00
parent f093f00a4b
commit 59afd3f0a5
15 changed files with 193 additions and 137 deletions

View File

@ -2,32 +2,28 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Input, Button, Modal, NgIf } from './';
import './prompt.scss';
import "./alert.scss";
export class Alert extends React.Component {
constructor(props){
super(props);
this.state = {
modal_appear: false
};
}
onSubmit(e){
e.preventDefault();
this.props.onConfirm();
this.setState({modal_appear: false});
e && e.preventDefault && e.preventDefault();
this.props.onConfirm && this.props.onConfirm();
}
render() {
return (
<Modal isActive={this.state.modal_appear} onQuit={this.onSubmit.bind(this)}>
<Modal isActive={this.props.appear} onQuit={this.onSubmit.bind(this)}>
<div className="component_alert">
<p>
<p className="modal-message">
{this.props.message}
</p>
<form id="key_manager" onSubmit={this.onSubmit.bind(this)}>
<form onSubmit={this.onSubmit.bind(this)}>
<div className="buttons">
<Button type="submit" onClick={this.onSubmit.bind(this)}>OK</Button>
<Button type="submit" theme="secondary" onClick={this.onSubmit.bind(this)}>OK</Button>
</div>
</form>
</div>
@ -37,6 +33,7 @@ export class Alert extends React.Component {
}
Alert.propTypes = {
appear: PropTypes.bool.isRequired,
message: PropTypes.string.isRequired,
onConfirm: PropTypes.func
};

View File

@ -0,0 +1,3 @@
.component_modal > div{
padding: 20px 20px 0 20px;
}

View File

@ -1,7 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom'
import { Link } from 'react-router-dom';
import { NgIf, Icon, EventEmitter, EventReceiver } from './';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import './breadcrumb.scss';
export class BreadCrumb extends React.Component {
constructor(props){
@ -26,7 +29,7 @@ export class BreadCrumb extends React.Component {
if(index === paths.length - 1){
return {full: null, label: label};
}else{
return {full: sub_path+'/', label: label}
return {full: sub_path+'/', label: label};
}
});
return paths;
@ -35,19 +38,21 @@ export class BreadCrumb extends React.Component {
render(Element) {
const Path = Element? Element : PathElement;
return (
<div>
<div className="component_breadcrumb">
<BreadCrumbContainer className={this.props.className+' no-select'}>
<Logout />
<ReactCSSTransitionGroup transitionName="breadcrumb" transitionLeave={true} transitionEnter={true} transitionLeaveTimeout={500} transitionEnterTimeout={500} transitionAppear={false}>
{
this.state.path.map((path, index) => {
return (
<span key={index}>
<span key={"breadcrumb_"+index}>
<Path path={path} isLast={this.state.path.length === index + 1} needSaving={this.props.needSaving} />
<Separator isLast={this.state.path.length === index + 1} />
</span>
)
);
})
}
</ReactCSSTransitionGroup>
</BreadCrumbContainer>
</div>
);
@ -77,7 +82,7 @@ const Logout = (props) => {
display: 'inline-block',
padding: '5px 0px 5px 5px',
margin: '0 0px'
}
};
return (
<li style={style}>
<Link to="/logout">
@ -176,6 +181,6 @@ export class PathElement extends PathElementWrapper {
<div style={{display: 'inline-block', color: this.props.isLast? '#6f6f6f' : 'inherit'}}>
<PathElementWrapper highlight={highlight} {...this.props} />
</div>
)
);
}
}

View File

@ -0,0 +1,23 @@
.component_breadcrumb{
.breadcrumb-leave{
display: inline-block;
opacity: 1;
transform: translateY(0px);
}
.breadcrumb-leave.breadcrumb-leave-active{
opacity: 0;
transform: translateY(-10px);
transition: all 0.15s ease-out;
}
.breadcrumb-enter{
transform: translateX(10px);
opacity: 0;
display: inline-block;
}
.breadcrumb-enter.breadcrumb-enter-active{
opacity: 1;
transform: translateX(0);
transition: all 0.2s ease-out;
}
}

View File

@ -6,6 +6,7 @@
right: 0;
background: rgba(0,0,0,0.7);
box-shadow: 1px 2px 10px rgba(0,0,0,0.2);
z-index: 1000;
> div{
background: white;
@ -16,6 +17,40 @@
}
}
.component_prompt, .component_alert{
> p{
font-size: 1.1em;
margin: 0 0 10px 0;
}
.modal-error-message{
color: var(--error);
}
.buttons{
display: flex;
[type="submit"]{
border-radius: 10px 0 0;
}
> button{
width: 50%;
margin-left: auto;
}
}
}
.component_prompt .buttons{
margin: 15px -20px;
> p{
text-align: center;
}
}
.component_alert .buttons{
margin: 25px -20px 15px -20px;
> p{
text-align: justify;
}
}
/***********************/
/* ENTERING TRANSITION */
// background

View File

@ -2,41 +2,48 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Input, Button, Modal, NgIf } from './';
import './prompt.scss';
export class Prompt extends React.Component {
constructor(props){
super(props);
this.state = {
modal_appear: false,
error: ''
error: this.props.error,
value: ''
};
if(this.props.error) window.setTimeout(() => this.setState({error: ''}), 2000);
}
componentWillReceiveProps(props){
if(props.error !== this.state.error){
this.setState({error: props.error});
window.setTimeout(() => this.setState({error: ''}), 2000);
}
}
onCancel(should_clear){
this.setState({modal_appear: false});
if(this.props.onCancel) this.props.onCancel();
}
onSubmit(e){
e && e.preventDefault && e.preventDefault();
if(this.props.onSubmit) this.props.onSubmit(this.state.value);
}
onInputChange(value){
this.setState({value: value});
}
render() {
return (
<Modal isActive={this.state.modal_appear} onQuit={this.onCancel.bind(this)}>
<Modal isActive={this.props.appear} onQuit={this.onCancel.bind(this)}>
<div className="component_prompt">
<p className="message">
<p className="modal-message">
{this.props.message}
</p>
<form onSubmit={this.onSubmit.bind(this)}>
<Input autoFocus={true} value={this.state.key} type={this.props.type || 'text'} onChange={this.onKeyChange.bind(this)} autoComplete="new-password" />
<Input autoFocus={true} value={this.state.value} type={this.props.type || 'text'} autoComplete="new-password" onChange={(e) => this.onInputChange(e.target.value)} />
<div className="error">{this.props.error}&nbsp;</div>
<div className="modal-error-message">{this.state.error}&nbsp;</div>
<div className="buttons">
<Button type="button" onClick={this.onCancel.bind(this)}>CANCEL</Button>
@ -50,8 +57,10 @@ export class Prompt extends React.Component {
}
Prompt.propTypes = {
appear: PropTypes.bool.isRequired,
type: PropTypes.string,
message: PropTypes.string.isRequired,
error: PropTypes.string,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
};

View File

@ -8,6 +8,7 @@ import { ForkMe, RememberMe, Credentials, Form } from './connectpage/';
import { invalidate } from '../helpers/';
import config from '../../config.js';
import { Alert, Prompt } from '../components/';
export class ConnectPage extends React.Component {
constructor(props){
@ -101,7 +102,6 @@ export class ConnectPage extends React.Component {
<NgIf cond={this.state.loading === true}>
<Loader/>
</NgIf>
<NgIf cond={this.state.loading === false}>
<ReactCSSTransitionGroup transitionName="form" transitionLeave={false} transitionEnter={false} transitionAppear={true} transitionAppearTimeout={500}>
<Form credentials={this.state.credentials}

View File

@ -2,9 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import { Input, Button, NgIf, Modal } from '../../components/';
import { Prompt } from '../../components/';
import { encrypt, decrypt, memory } from '../../helpers/';
import './credentials.scss';
export class Credentials extends React.Component {
constructor(props){
@ -13,7 +12,7 @@ export class Credentials extends React.Component {
this.state = {
modal_appear: key ? false : this.props.remember_me,
key: key || '',
message: null,
message: '',
error: null
};
// we use a clojure for the "key" because we want to persist it in memory
@ -38,7 +37,7 @@ export class Credentials extends React.Component {
componentDidMount(){
this.init();
if(this.state.key) this.onSubmit();
if(this.state.key) this.onSubmit(this.state.key);
}
init(){
@ -57,24 +56,19 @@ export class Credentials extends React.Component {
}
}
onKeyChange(e){
this.setState({key: e.target.value});
memory.set('credentials_key', e.target.value);
}
onCancel(should_clear){
memory.set('credentials_key', '');
this.setState({modal_appear: false, key: ''});
}
onSubmit(e){
e && e.preventDefault();
onSubmit(key){
this.setState({key: key});
memory.set('credentials_key', key);
/*
* 2 differents use cases:
* - a user is creating a new master password
* - a user want to unlock existing credentials
*/
const key = memory.get('credentials_key');
if(key !== ''){
let raw = window.localStorage.getItem('credentials');
if(raw){
@ -97,23 +91,14 @@ export class Credentials extends React.Component {
render() {
return (
<Modal isActive={this.state.modal_appear} onQuit={this.onCancel.bind(this)}>
<div className="component_password">
<p>
{this.state.message}
</p>
<form id="key_manager" onSubmit={this.onSubmit.bind(this)}>
<Input autoFocus={true} value={this.state.key} type="password" onChange={this.onKeyChange.bind(this)} autoComplete="new-password" />
<div key={this.state.error} className="error">{this.state.error}&nbsp;</div>
<div className="buttons">
<Button type="button" onClick={this.onCancel.bind(this)}>CANCEL</Button>
<Button type="submit" theme="secondary">OK</Button>
</div>
</form>
</div>
</Modal>
<Prompt
type="password"
appear={this.state.modal_appear}
error={this.state.error}
message={this.state.message}
onCancel={this.onCancel.bind(this)}
onSubmit={this.onSubmit.bind(this)}
/>
);
}
}

View File

@ -1,43 +0,0 @@
.component_password{
> p{
text-align: center;
font-size: 1.1em;
margin: 0 0 10px 0;
}
.error{
color: var(--error);
}
.buttons{
display: flex;
margin: 15px -20px;
[type="submit"]{
border-radius: 10px 0 0;
}
}
}
/***********************/
/* APPEAR TRANSITION */
.form-appear{ opacity: 0;}
.form-appear.form-appear-active{
opacity: 1;
transition: opacity 0.1s ease;
}
/***********************/
/* ENTER TRANSITION */
.form-enter{ opacity: 0;}
.form-enter.form-enter-active{
opacity: 1;
transition: opacity 0.1s ease;
}
/***********************/
/* LEAVE TRANSITION */
.form-leave{ opacity: 1;}
.form-leave.form-enter-active{
opacity: 0;
transition: opacity 0.5s ease-out;
}

View File

@ -20,11 +20,12 @@ export class Form extends React.Component {
advanced_git: false,
dummy: true
};
this.rerender = this.rerender.bind(this);
}
componentDidMount(){
this.publishState(this.props.credentials);
window.addEventListener('resize', this.rerender.bind(this));
window.addEventListener('resize', this.rerender);
}
componentWillReceiveProps(props){
@ -34,7 +35,7 @@ export class Form extends React.Component {
}
componentWillUnmount(){
window.removeEventListener('resize', this.rerender.bind(this));
window.removeEventListener('resize', this.rerender);
}
publishState(_credentials){

View File

@ -97,7 +97,6 @@ export class FilesPage extends React.Component {
return Files.rm(file, type);
}
onUpload(path, files){
const createFilesInUI = (_files) => {
const newfiles = _files.map((file) => {

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { DragSource, DropTarget } from 'react-dnd';
import './existingthing.scss';
import { Card, NgIf, Icon, EventEmitter} from '../../components/';
import { Card, NgIf, Icon, EventEmitter, Prompt } from '../../components/';
import { pathBuilder } from '../../helpers/';
const fileSource = {
@ -93,7 +93,9 @@ export class ExistingThing extends React.Component {
message: null,
icon: props.file.type,
filename: props.file.name,
request_delete: false
delete_request: false,
delete_message: "Confirm by tapping \""+this._confirm_delete_text()+"\"",
delete_error: ''
};
}
@ -132,13 +134,11 @@ export class ExistingThing extends React.Component {
}
onDeleteRequest(filename){
let toConfirm = this.props.file.name.length > 16? this.props.file.name.substring(0, 10).toLowerCase() : this.props.file.name;
let answer = prompt('Confirm by tapping "'+toConfirm+'"');
console.log(answer);
this.setState({delete_request: true});
}
onDeleteConfirm(filename, toConfirm, answer){
if(answer === toConfirm){
this.setState({icon: 'loading'});
onDeleteConfirm(answer){
if(answer === this._confirm_delete_text()){
this.setState({icon: 'loading', delete_request: false});
this.props.emit(
'file.delete',
pathBuilder(this.props.path, this.props.file.name),
@ -149,8 +149,16 @@ export class ExistingThing extends React.Component {
if(err && err.code === 'CANCELLED'){ return; }
this.setState({icon: 'error', message: err.message});
});
}else{
this.setState({delete_error: "Doesn't match"});
}
}
onDeleteCancel(){
this.setState({delete_request: false});
}
_confirm_delete_text(){
return this.props.file.name.length > 16? this.props.file.name.substring(0, 10).toLowerCase() : this.props.file.name;
}
render(highlight){
@ -182,14 +190,11 @@ export class ExistingThing extends React.Component {
<Message message={this.state.message} />
</Card>
</NgIf>
<NgIf cond={this.state.request_delete}>
</NgIf>
<Prompt appear={this.state.delete_request} error={this.state.delete_error} message={this.state.delete_message} onCancel={this.onDeleteCancel.bind(this)} onSubmit={this.onDeleteConfirm.bind(this)}/>
</div>
)));
}
}
// <Prompt message="" onCancel={() => {}} onConfirm={this.onDeleteConfirm.bind(this, this.state.filename, 'test')} />
ExistingThing.PropTypes = {
connectDragSource: PropTypes.func.isRequired,
isDragging: PropTypes.bool.isRequired,
@ -268,7 +273,7 @@ const DateTime = (props) => {
function padding(number){
let str = String(number),
pad = "00";
return pad.substring(0, pad.length - str.length) + str
return pad.substring(0, pad.length - str.length) + str;
}
if(timestamp){
let t = new Date(timestamp);
@ -298,7 +303,7 @@ const FileSize = (props) => {
}else if(bytes < 1099511627776){
return Math.round(bytes/(1024*1024*1024)*10)/10+'GB';
}else{
return Math.round(bytes/(1024*1024*1024*1024))+'TB'
return Math.round(bytes/(1024*1024*1024*1024))+'TB';
}
}
const style = {color: '#6f6f6f', fontSize: '0.85em'};
@ -307,7 +312,7 @@ const FileSize = (props) => {
<NgIf cond={props.type === 'file'} style={{display: 'inline-block'}}>
<span style={style}>({displaySize(props.size)})</span>
</NgIf>
)
);
}
const Message = (props) => {
const style = {color: 'rgba(0,0,0,0.4)', fontSize: '0.9em', paddingLeft: '10px', display: 'inline'};
@ -315,5 +320,5 @@ const Message = (props) => {
<NgIf cond={props.message !== null} style={style}>
- {props.message}
</NgIf>
)
);
}

View File

@ -1,8 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import { DropTarget } from 'react-dnd';
import Path from 'path';
import "./filesystem.scss";
import { Container, NgIf } from '../../components/';
import { NewThing } from './newthing';
import { ExistingThing } from './existingthing';
@ -86,13 +88,14 @@ export class FileSystem extends React.Component {
render() {
return this.props.connectDropFile(
<div style={{height: '100%'}}>
<Container style={{height: '100%'}}>
<div className="component_filesystem">
<Container>
<NewThing path={this.props.path} sort={this.state.sort} onSortUpdate={(value) => {this.setState({sort: value})}} accessRight={this.state.access_right}></NewThing>
<NgIf cond={this.props.fileIsOver}>
<FileZone path={this.props.path} />
</NgIf>
<NgIf cond={this.props.files.length > 0} style={{clear: 'both', paddingBottom: '15px'}}>
<NgIf className="list" cond={this.props.files.length > 0}>
<ReactCSSTransitionGroup transitionName="filelist-item" transitionLeave={false} transitionEnter={false} transitionAppear={true} transitionAppearTimeout={200}>
{
this.sort(this.props.files, this.state.sort).map((file, index) => {
if(file.type === 'directory' || file.type === 'file' || file.type === 'link' || file.type === 'bucket'){
@ -100,8 +103,9 @@ export class FileSystem extends React.Component {
}
})
}
</ReactCSSTransitionGroup>
</NgIf>
<NgIf cond={this.props.files.length === 0 && !this.state.creating} style={{fontSize: '25px', textAlign: 'center', fontWeight: '100', marginTop: '50px'}}>
<NgIf className="error" cond={this.props.files.length === 0 && !this.state.creating}>
There is nothing here
</NgIf>
</Container>

View File

@ -0,0 +1,27 @@
.filelist-item-appear{
opacity: 0;
transform: translateY(5px);
}
.filelist-item-appear.filelist-item-appear-active{
transition: all 0.2s ease-out;
transform: translateY(0px);
opacity: 1;
}
.component_filesystem{
height: 100%;
> div{
height: 100%;
.list{
clear: both;
padding-bottom: 150px;
}
.error{
font-size: 25px;
text-align: center;
font-weight: 100;
margin-top: 50px;
}
}
}

View File

@ -2,7 +2,7 @@
height: 100%;
color: #333333;
}
.CodeMirror-scroll { -webkit-overflow-scrolling: touch; }
/* SEARCH */
.CodeMirror-dialog {
@ -38,20 +38,26 @@
font-size: 70%;
}
.CodeMirror-scroll { -webkit-overflow-scrolling: touch; }
/* Highlight Theme */
.cm-s-default .cm-header {color: #3E7AA6; font-size: 1.15em;}
.cm-s-default .cm-keyword {color: #3E7AA6;}
.cm-s-default .cm-header.cm-org-level-star{color: #6f6f6f; position: relative; top: 2px;}
.cm-s-default .cm-header.cm-org-todo{color: #FF8355;}
.cm-s-default .cm-header.cm-org-done{color: #3BB27C;}
.cm-s-default .cm-link{color: #555!important;}
.cm-s-default .cm-url{color: #555!important;}
.cm-s-default .cm-keyword {color: #3E7AA6;}
.cm-s-default .cm-variable-3 {color: #085;}
.cm-s-default .cm-comment {color: #6f6f6f;}
.cm-s-default .cm-string, .cm-s-default .cm-string-2{ color: #c41a16; }
.cm-s-default .cm-def { color: rgb(68, 85, 136); }
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
.CodeMirror-foldmarker{
padding-left: 5px;
color: #6f6f6f;
text-shadow: none;
}