improvements (UI): #18 + connection page improvements

This commit is contained in:
Mickael KERJEAN 2018-03-01 05:03:48 +11:00
parent 5b2a0330b6
commit 73c1f9be4a
46 changed files with 2760 additions and 439 deletions

View file

@ -3,4 +3,3 @@ export { Session } from './api';
export { invalidate } from './tools';
export { opener } from './mimetype';
export { EventEmitter, EventReceiver } from './events';
export { password } from './password';

View file

@ -1,14 +0,0 @@
function keyManager(){
let key = null;
return {
get: function(){
return key;
},
set: function(_key){
key = _key || null
}
}
}
export const password = keyManager();

View file

@ -1,26 +1,26 @@
import React from 'react';
import { Container, Card, NgIf, Input, Button, Textarea, Loader, Notification, encrypt, decrypt, theme } from '../utilities';
import { Session, invalidate, password } from '../data';
import { Uploader } from '../utilities';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import config from '../../config.js';
import { Container, NgIf, Loader, Notification, theme } from '../utilities/';
import { Session, invalidate } from '../data';
import { ForkMe, RememberMe, Credentials, Form } from './connectpage/';
import './connectpage.scss';
export class ConnectPage extends React.Component {
constructor(props){
super(props);
this.state = {
type: 'webdav',
credentials: {},
remember_me: window.localStorage.hasOwnProperty('credentials') ? true : false,
loading: false,
error: null,
advanced_ftp: false, // state of checkbox in the UI
advanced_sftp: false, // state of checkbox in the UI
advanced_webdav: false,
advanced_s3: false,
advanced_git: false,
credentials: {},
password: password.get() || null,
marginTop: this._marginTop()
}
};
// adapt from: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
// adapted from: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
function getParam(name) {
const regex = new RegExp("[?&#]" + name.replace(/[\[\]]/g, "\\$&") + "(=([^&#]*)|&|#|$)");
const results = regex.exec(window.location.href);
@ -32,101 +32,60 @@ export class ConnectPage extends React.Component {
// dropbox login
if(getParam('state') === 'dropbox'){
this.state.loading = true;
this.authenticate({bearer: getParam('access_token'), type: 'dropbox'})
}
this.authenticate({bearer: getParam('access_token'), type: 'dropbox'});
}
// google drive login
if(getParam('code')){
this.state.loading = true;
this.authenticate({code: getParam('code'), type: 'gdrive'})
this.authenticate({code: getParam('code'), type: 'gdrive'});
}
}
componentWillMount(){
window.onresize = () => {
this.setState({marginTop: this._marginTop()});
};
}
_marginTop(){
let size = Math.round(Math.abs((document.body.offsetHeight - 300) / 2));
return size > 150? 150 : size;
}
authenticate(params){
if(params.type === 'dropbox') return this.login_dropbox();
else if(params.type === 'gdrive') return this.login_google();
componentWillMount(){
window.onresize = () => {
this.setState({marginTop: this._marginTop()})
}
let raw = window.localStorage.getItem('store');
if(!this.state.loading && raw){
if(this.state.password === null){
let key = prompt("Your password: ");
if(key){
password.set(key);
let credentials = decrypt(raw, key);
this.setState({password: password, credentials: credentials}, setAdvanced.bind(this));
}
}else{
let credentials = decrypt(raw, this.state.password);
this.setState({credentials: credentials}, setAdvanced.bind(this));
}
function setAdvanced(){
if(this.state.credentials['ftp'] && (this.state.credentials['ftp']['path'] || this.state.credentials['ftp']['port']) ){
this.setState({advanced_ftp: true})
}
if(this.state.credentials['sftp'] && (this.state.credentials['sftp']['path'] || this.state.credentials['sftp']['port'] || this.state.credentials['sftp']['private_key'])){
this.setState({advanced_sftp: true})
}
if(this.state.credentials['webdav'] && this.state.credentials['webdav']['path']){
this.setState({advanced_webdav: true})
}
if(this.state.credentials['s3'] && this.state.credentials['s3']['path']){
this.setState({advanced_s3: true})
}
if(this.state.credentials['git'] && (this.state.credentials['git']['username'] || this.state.credentials['git']['commit'] || this.state.credentials['git']['branch'] || this.state.credentials['git']['passphrase'] || this.state.credentials['git']['author_name'] || this.state.credentials['git']['author_email'] || this.state.credentials['git']['committer_name'] || this.state.credentials['git']['committer_email'])){
this.setState({advanced_git: true})
}
}
}
}
getDefault(type, key){
if(this.state.credentials[type]){
return this.state.credentials[type][key]
}else{
return null;
}
}
onRememberMe(e){
let value = e.target.checked;
if(value === true){
let key = prompt("password that will serve to encrypt your credentials:");
password.set(key);
this.setState({password: key});
}else if(value === false){
window.localStorage.clear();
password.set();
this.setState({credentials: {}, password: null});
}
}
onChange(type){
this.setState({type: type});
this.setState({loading: true});
Session.authenticate(params)
.then((ok) => {
this.setState({loading: false});
invalidate();
const path = params.path && /^\//.test(params.path)? /\/$/.test(params.path) ? params.path : params.path+'/' : '/';
this.props.history.push('/files'+path);
})
.catch(err => {
if(err && err.code === 'CANCELLED'){ return; }
this.setState({loading: false, error: err});
window.setTimeout(() => {
this.setState({error: null});
}, 1000);
});
}
login_dropbox(e){
e.preventDefault();
this.setState({loading: true});
Session.url('dropbox').then((url) => {
window.location.href = url;
}).catch((err) => {
if(err && err.code === 'CANCELLED'){ return }
if(err && err.code === 'CANCELLED'){ return; }
this.setState({loading: false, error: err});
window.setTimeout(() => {
this.setState({error: null})
this.setState({error: null});
}, 1000);
});
}
login_google(e){
e.preventDefault();
login_google(e){
this.setState({loading: true});
Session.url('gdrive').then((url) => {
window.location.href = url;
@ -134,190 +93,55 @@ export class ConnectPage extends React.Component {
if(err && err.code === 'CANCELLED'){ return }
this.setState({loading: false, error: err});
window.setTimeout(() => {
this.setState({error: null})
this.setState({error: null});
}, 1000);
})
});
}
authenticate(params){
if(password.get()){
this.state.credentials[params['type']] = params;
window.localStorage.setItem('store', encrypt(this.state.credentials, password.get()));
}
Session.authenticate(params)
.then((ok) => {
this.setState({loading: false});
invalidate();
const path = params.path && /^\//.test(params.path)? /\/$/.test(params.path) ? params.path : params.path+'/' : '/';
this.props.history.push('/files'+path);
})
.catch(err => {
if(err && err.code === 'CANCELLED'){ return }
this.setState({loading: false, error: err});
window.setTimeout(() => {
this.setState({error: null})
}, 1000);
});
}
onSubmit(e){
e.preventDefault();
this.setState({loading: true});
// yes it's dirty but at least it's supported nearly everywhere and build won't push Megabytes or polyfill
// to support the entries method of formData which would have made things much cleaner
const serialize = function($form){
if(!$form) return {};
var obj = {};
var elements = $form.querySelectorAll( "input, select, textarea" );
for( var i = 0; i < elements.length; ++i ) {
var element = elements[i];
var name = element.name;
var value = element.value;
if(name){
obj[name] = value;
}
}
return obj;
}
const data = serialize(document.querySelector('form'));
onFormSubmit(data, credentials){
this.setState({credentials: credentials});
this.authenticate(data);
}
setRemember(state){
this.setState({remember_me: state});
}
setCredentials(creds){
this.setState({credentials: creds});
}
render() {
let labelStyle = {color: 'rgba(0,0,0,0.4)', fontStyle: 'italic', fontSize: '0.9em'}
let style = {
top: {minWidth: '80px', borderTopLeftRadius: 0, borderTopRightRadius: 0, padding: '8px 5px'}
}
return (
<div style={{background: theme.colors.primary}}>
<ForkMe repo="https://github.com/mickael-kerjean/nuage" />
<Container maxWidth="565px">
<NgIf cond={this.state.loading === true}>
<Loader/>
<div className="component_page_connect">
<NgIf cond={config.fork_button}>
<ForkMe repo="https://github.com/mickael-kerjean/nuage" />
</NgIf>
<NgIf cond={this.state.loading === false}>
<Card style={{marginTop: this.state.marginTop+'px', whiteSpace: '', borderRadius: '3px', boxShadow: 'none'}}>
<div style={{display: 'flex', margin: '-10px -11px 20px', padding: '0px 0px 6px 0'}} className={window.innerWidth < 600 ? 'scroll-x' : ''}>
<Button theme={this.state.type === 'webdav'? 'primary' : null} style={{...style.top, borderBottomLeftRadius: 0}} onClick={this.onChange.bind(this, 'webdav')}>WebDav</Button>
<Button theme={this.state.type === 'ftp'? 'primary' : null} style={style.top} onClick={this.onChange.bind(this, 'ftp')}>FTP</Button>
<Button theme={this.state.type === 'sftp'? 'primary' : null} style={style.top} onClick={this.onChange.bind(this, 'sftp')}>SFTP</Button>
<Button theme={this.state.type === 'git'? 'primary' : null} style={{...style.top, borderBottomRightRadius: 0}} onClick={this.onChange.bind(this, 'git')}>Git</Button>
<Button theme={this.state.type === 's3'? 'primary' : null} style={style.top} onClick={this.onChange.bind(this, 's3')}>S3</Button>
<Button theme={this.state.type === 'dropbox'? 'primary' : null} style={style.top} onClick={this.onChange.bind(this, 'dropbox')}>Dropbox</Button>
<Button theme={this.state.type === 'gdrive'? 'primary' : null} style={{...style.top, borderBottomRightRadius: 0}} onClick={this.onChange.bind(this, 'gdrive')}>Drive</Button>
</div>
<div>
<form onSubmit={this.onSubmit.bind(this)} autoComplete="off" autoCapitalize="off" spellCheck="false" autoCorrect="off">
<NgIf cond={this.state.type === 'webdav'}>
<Input type="text" name="url" placeholder="Address*" defaultValue={this.getDefault('webdav', 'url')} autoComplete="off" />
<Input type="text" name="username" placeholder="Username" defaultValue={this.getDefault('webdav', 'username')} autoComplete="off" />
<Input type="password" name="password" placeholder="Password" defaultValue={this.getDefault('webdav', 'password')} autoComplete="off" />
<label style={labelStyle}>
<input checked={this.state.advanced_webdav} onChange={e => { this.setState({advanced_webdav: e.target.checked})}} type="checkbox" autoComplete="off"/> Advanced
</label>
<NgIf cond={this.state.advanced_webdav === true} style={{marginTop: '2px'}}>
<Input type="text" name="path" placeholder="Path" defaultValue={this.getDefault('webdav', 'path')} autoComplete="off" />
</NgIf>
<Input type="hidden" name="type" value="webdav"/>
<Button style={{marginTop: '15px', color: 'white'}} theme="emphasis">CONNECT</Button>
</NgIf>
<NgIf cond={this.state.type === 'ftp'}>
<Input type="text" name="hostname" placeholder="Hostname*" defaultValue={this.getDefault('ftp', 'hostname')} autoComplete="off" />
<Input type="text" name="username" placeholder="Username" defaultValue={this.getDefault('ftp', 'username')} autoComplete="off" />
<Input type="password" name="password" placeholder="Password" defaultValue={this.getDefault('ftp', 'password')} autoComplete="off" />
<Input type="hidden" name="type" value="ftp"/>
<label style={labelStyle}>
<input checked={this.state.advanced_ftp} onChange={e => { this.setState({advanced_ftp: e.target.checked})}} type="checkbox" autoComplete="off"/> Advanced
</label>
<NgIf cond={this.state.advanced_ftp === true} style={{marginTop: '2px'}}>
<Input type="text" name="path" placeholder="Path" defaultValue={this.getDefault('ftp', 'path')} autoComplete="off" />
<Input type="text" name="port" placeholder="Port" defaultValue={this.getDefault('ftp', 'port')} autoComplete="off" />
</NgIf>
<Button style={{marginTop: '15px', color: 'white'}} theme="emphasis">CONNECT</Button>
</NgIf>
<NgIf cond={this.state.type === 'sftp'}>
<Input type="text" name="host" placeholder="Hostname*" defaultValue={this.getDefault('sftp', 'host')} autoComplete="off" />
<Input type="text" name="username" placeholder="Username" defaultValue={this.getDefault('sftp', 'username')} autoComplete="off" />
<Input type="password" name="password" placeholder="Password" defaultValue={this.getDefault('sftp', 'password')} autoComplete="off" />
<Input type="hidden" name="type" value="sftp"/>
<label style={labelStyle}>
<input checked={this.state.advanced_sftp} onChange={e => { this.setState({advanced_sftp: JSON.parse(e.target.checked)})}} type="checkbox" autoComplete="off"/> Advanced
</label>
<NgIf cond={this.state.advanced_sftp === true} style={{marginTop: '2px'}}>
<Input type="text" name="path" placeholder="Path" defaultValue={this.getDefault('sftp', 'path')} autoComplete="off" />
<Input type="text" name="port" placeholder="Port" defaultValue={this.getDefault('sftp', 'port')} autoComplete="off" />
<Textarea type="text" name="private_key" placeholder="Private Key" defaultValue={this.getDefault('sftp', 'private_key')} autoComplete="off" />
</NgIf>
<Button style={{marginTop: '15px', color: 'white'}} theme="emphasis">CONNECT</Button>
</NgIf>
<NgIf cond={this.state.type === 'git'}>
<Input type="text" name="repo" placeholder="Repository*" defaultValue={this.getDefault('git', 'repo')} autoComplete="off" />
<Textarea type="password" name="password" placeholder="Password" defaultValue={this.getDefault('git', 'password')} autoComplete="off" />
<Input type="hidden" name="type" value="git"/>
<label style={labelStyle}>
<input checked={this.state.advanced_git} onChange={e => { this.setState({advanced_git: JSON.parse(e.target.checked)})}} type="checkbox" autoComplete="off"/> Advanced
</label>
<NgIf cond={this.state.advanced_git === true} style={{marginTop: '2px'}}>
<Input type="text" name="username" placeholder="Username" defaultValue={this.getDefault('git', 'username')} autoComplete="off" />
<Input type="text" name="passphrase" placeholder="Passphrase" defaultValue={this.getDefault('git', 'passphrase')} autoComplete="off" />
<Input type="text" name="commit" placeholder="Commit Format: default to '{action}({filename}): {path}'" defaultValue={this.getDefault('git', 'format')} autoComplete="off" />
<Input type="text" name="branch" placeholder="Branch: default to 'master'" defaultValue={this.getDefault('git', 'branch')} autoComplete="off" />
<Input type="text" name="author_email" placeholder="Author email" defaultValue={this.getDefault('git', 'author_email')} autoComplete="off" />
<Input type="text" name="author_name" placeholder="Author name" defaultValue={this.getDefault('git', 'author_name')} autoComplete="off" />
<Input type="text" name="committer_email" placeholder="Committer email" defaultValue={this.getDefault('git', 'committer_email')} autoComplete="off" />
<Input type="text" name="committer_name" placeholder="Committer name" defaultValue={this.getDefault('git', 'committer_name')} autoComplete="off" />
</NgIf>
<Button style={{marginTop: '15px', color: 'white'}} theme="emphasis">CONNECT</Button>
</NgIf>
<NgIf cond={this.state.type === 's3'}>
<Input type="text" name="access_key_id" placeholder="Access Key ID*" defaultValue={this.getDefault('s3', 'access_key_id')} autoComplete="off" />
<Input type="password" name="secret_access_key" placeholder="Secret Access Key*" defaultValue={this.getDefault('s3', 'secret_access_key')} autoComplete="off" />
<Input type="hidden" name="type" value="s3"/>
<label style={labelStyle}>
<input checked={this.state.advanced_s3} onChange={e => { this.setState({advanced_s3: JSON.parse(e.target.checked)})}} type="checkbox" autoComplete="off"/> Advanced
</label>
<NgIf cond={this.state.advanced_s3 === true} style={{marginTop: '2px'}}>
<Input type="text" name="path" placeholder="Path" defaultValue={this.getDefault('s3', 'path')} autoComplete="off" />
</NgIf>
<Button style={{marginTop: '15px', color: 'white'}} theme="emphasis">CONNECT</Button>
</NgIf>
<NgIf cond={this.state.type === 'dropbox'}>
<a target="_blank" href={this.state.dropbox_url}>
<div style={{textAlign: 'center'}} onClick={this.login_dropbox.bind(this)}>
<img src="/img/dropbox.png" style={{height: '115px', margin: '10px 0'}}/>
</div>
<Input type="hidden" name="type" value="dropbox"/>
<Button onClick={this.login_dropbox.bind(this)} style={{color: 'white'}} theme="emphasis">LOGIN WITH DROPBOX</Button>
</a>
</NgIf>
<NgIf cond={this.state.type === 'gdrive'}>
<div style={{textAlign: 'center'}} onClick={this.login_google.bind(this)}>
<img src="/img/google-drive.png" style={{height: '115px', margin: '10px 0'}}/>
</div>
<Input type="hidden" name="type" value="gdrive"/>
<Button onClick={this.login_google.bind(this)} style={{color: 'white'}} theme="emphasis">LOGIN WITH GOOGLE</Button>
</NgIf>
</form>
</div>
</Card>
<label style={{ ...labelStyle, display: 'inline-block', width: '100%', textAlign: 'right'}}>
<input checked={this.state.password !== null} onChange={this.onRememberMe.bind(this)} type="checkbox"/> Remember me
</label>
</NgIf>
<Notification error={this.state.error && this.state.error.message} />
</Container>
<Container maxWidth="565px">
<NgIf cond={this.state.loading === true}>
<Loader/>
</NgIf>
<ReactCSSTransitionGroup
transitionName="form"
transitionLeave={false}
transitionEnter={false}
transitionAppear={true} transitionAppearTimeout={500}
>
<NgIf key={"form"+this.state.loading} cond={this.state.loading === false}>
<Form
credentials={this.state.credentials}
onSubmit={this.onFormSubmit.bind(this)} />
<RememberMe state={this.state.remember_me} onChange={this.setRemember.bind(this)}/>
</NgIf>
</ReactCSSTransitionGroup>
<Credentials remember_me={this.state.remember_me}
onRememberMeChange={this.setRemember.bind(this)}
onCredentialsFound={this.setCredentials.bind(this)}
credentials={this.state.credentials} />
<Notification error={this.state.error && this.state.error.message} />
</Container>
</div>
);
}
}
const ForkMe = (props) => {
return (
<a href={props.repo} target="_blank">
<img style={{position: 'absolute', top: 0, right: 0, border: 0}} src="https://camo.githubusercontent.com/52760788cde945287fbb584134c4cbc2bc36f904/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f77686974655f6666666666662e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_white_ffffff.png" />
</a>
);
}

View file

@ -0,0 +1,3 @@
.component_page_connect{
background: var(--primary);
}

View file

@ -0,0 +1,122 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import { Input, Button, Modal, NgIf } from '../../utilities';
import { encrypt, decrypt, memory } from '../../utilities';
import './credentials.scss';
export class Credentials extends React.Component {
constructor(props){
super(props);
const key = memory.get('credentials_key') || '';
this.state = {
modal_appear: key ? false : this.props.remember_me,
key: key || '',
message: null,
error: null
};
// we use a clojure for the "key" because we want to persist it in memory
// not just in the state which is kill whenever react decide
}
componentWillReceiveProps(new_props){
if(new_props.remember_me === false){
window.localStorage.clear();
}else if(new_props.remember_me === true){
this.saveCreds(new_props.credentials);
}
if(new_props.remember_me === true && this.props.remember_me === false){
this.setState({modal_appear: true});
this.init();
}else if(new_props.remember_me === false && this.props.remember_me === true){
memory.set('credentials_key', '');
this.setState({modal_appear: false, key: ''});
}
}
componentWillMount(){
this.init();
if(this.state.key) this.onSubmit();
}
init(){
let raw = window.localStorage.hasOwnProperty('credentials');
if(raw){
this.setState({message: "Your Master Password:"});
}else{
this.setState({message: "Pick a Master Password:"});
}
}
saveCreds(creds){
const key = memory.get('credentials_key');
if(key){
window.localStorage.setItem('credentials', encrypt(creds, key));
}
}
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();
/*
* 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){
try{
let credentials = decrypt(raw, key);
this.setState({modal_appear: false});
this.props.onCredentialsFound(credentials);
}catch(e){
this.setState({error: 'Incorrect password'});
}
}else{
this.saveCreds(this.props.credentials);
this.setState({modal_appear: false});
}
}else{
this.setState({error: 'Password can\'t be empty'});
}
window.setTimeout(() => this.setState({error: null}), 1500);
}
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>
);
}
}
Credentials.propTypes = {
};

View file

@ -0,0 +1,43 @@
.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

@ -0,0 +1,10 @@
import React from 'react';
import './forkme.scss';
export const ForkMe = (props) => {
return (
<a href={props.repo} target="_blank" className="component-forkme">
<img src="https://camo.githubusercontent.com/52760788cde945287fbb584134c4cbc2bc36f904/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f77686974655f6666666666662e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_white_ffffff.png" />
</a>
);
};

View file

@ -0,0 +1,8 @@
.component-forkme{
img{
position: absolute;
top: 0;
right: 0;
border: 0;
}
}

View file

@ -0,0 +1,218 @@
import React from 'react';
import { Container, Card, NgIf, Input, Button, Textarea, Loader, Notification, encrypt, decrypt, theme, Prompt } from '../../utilities';
import { Session, invalidate, password } from '../../data';
import './form.scss';
export class Form extends React.Component {
constructor(props){
super(props);
this.state = {
refs: {},
type: 'sftp',
advanced_ftp: false,
advanced_sftp: false,
advanced_webdav: false,
advanced_s3: false,
advanced_git: false
};
}
_marginTop(){
let size = 300;
const $screen = document.querySelector('.login-form');
if($screen) size = $screen.offsetHeight;
size = Math.round((document.body.offsetHeight - size) / 2);
if(size < 0) return 0;
if(size > 150) return 150;
return size;
}
componentDidMount(){
this.publishState(this.props.credentials);
}
componentWillReceiveProps(props){
if(JSON.stringify(props.credentials) !== JSON.stringify(this.props.credentials)){
this.publishState(props.credentials);
}
}
publishState(_credentials){
const pushDOM = (credentials) => {
for(let key in credentials){
let names = credentials[key];
for(let name in names){
const ref_name = [key,name].join("_");
if(this.state.refs[ref_name]){
this.state.refs[ref_name].ref.value = credentials[key][name];
}
}
}
};
const setAdvancedCheckbox = (credentials) => {
if(credentials['ftp'] && (credentials['ftp']['path'] || credentials['ftp']['port']) ){
this.setState({advanced_ftp: true});
}
if(credentials['sftp'] && (
credentials['sftp']['path'] || credentials['sftp']['port']
|| credentials['sftp']['private_key'])
){
this.setState({advanced_sftp: true});
}
if(credentials['webdav'] && credentials['webdav']['path']){
this.setState({advanced_webdav: true});
}
if(credentials['s3'] && credentials['s3']['path']){
this.setState({advanced_s3: true});
}
if(credentials['git'] && (
credentials['git']['username'] || credentials['git']['commit']
|| credentials['git']['branch'] || credentials['git']['passphrase']
|| credentials['git']['author_name'] || credentials['git']['author_email']
|| credentials['git']['committer_name'] || credentials['git']['committer_email'])
){
this.setState({advanced_git: true});
}
};
setAdvancedCheckbox(_credentials);
window.setTimeout(() => pushDOM(_credentials));
// we made this async as DOM needs to be all set before we can insert the new state
}
onSubmit(e){
e.preventDefault();
// update the credentials object with data coming from the dom (aka "ref" in react language)
let credentials = Object.assign({}, this.props.credentials);
for(let key in this.state.refs){
if(this.state.refs[key]){
let [type, name] = key.split('_');
if(!credentials[type]) credentials[type] = {};
credentials[type][name] = this.state.refs[key].ref.value;
}
}
// create the object we need to authenticate a user against a backend
const auth_data = Object.assign({type: this.state.type}, credentials[this.state.type]);
this.props.onSubmit(auth_data, credentials);
}
onTypeChange(type){
this.setState({type: type}, () => this.publishState(this.props.credentials));
}
render() {
let className = (window.innerWidth < 600) ? 'scroll-x' : '';
return (
<Card style={{marginTop: this._marginTop()+'px'}} className="no-select component_page_connection_form">
<div className={"buttons "+className}>
<Button className={this.state.type === 'webdav'? 'active primary' : ''} onClick={this.onTypeChange.bind(this, 'webdav')} style={{borderBottomLeftRadius: 0}}>WebDav</Button>
<Button className={this.state.type === 'ftp'? 'active primary' : ''} onClick={this.onTypeChange.bind(this, 'ftp')}>FTP</Button>
<Button className={this.state.type === 'sftp'? 'active primary' : ''} onClick={this.onTypeChange.bind(this, 'sftp')}>SFTP</Button>
<Button className={this.state.type === 'git'? 'active primary' : ''} onClick={this.onTypeChange.bind(this, 'git')}>Git</Button>
<Button className={this.state.type === 's3'? 'active primary' : ''} onClick={this.onTypeChange.bind(this, 's3')}>S3</Button>
<Button className={this.state.type === 'dropbox'? 'active primary' : ''} onClick={this.onTypeChange.bind(this, 'dropbox')}>Dropbox</Button>
<Button className={this.state.type === 'gdrive'? 'active primary' : ''} onClick={this.onTypeChange.bind(this, 'gdrive')} style={{borderBottomRightRadius: 0}}>Drive</Button>
</div>
<div>
<form onSubmit={this.onSubmit.bind(this)} autoComplete="off" autoCapitalize="off" spellCheck="false" autoCorrect="off">
<NgIf cond={this.state.type === 'webdav'}>
<Input type="text" name="url" placeholder="Address*" ref={(input) => {this.state.refs.webdav_url = input; }} autoComplete="new-password" />
<Input type="text" name="username" placeholder="Username" ref={(input) => {this.state.refs.webdav_username = input; }} autoComplete="new-password" />
<Input type="password" name="password" placeholder="Password" ref={(input) => {this.state.refs.webdav_password = input; }} autoComplete="new-password" />
<label>
<input checked={this.state.advanced_webdav} onChange={e => { this.setState({advanced_webdav: e.target.checked}); }} type="checkbox" autoComplete="new-password"/> Advanced
</label>
<NgIf cond={this.state.advanced_webdav === true} className="advanced_form">
<Input type="text" name="path" placeholder="Path" ref={(input) => {this.state.refs.webdav_path = input; }} autoComplete="new-password" />
</NgIf>
<Input type="hidden" name="type" value="webdav"/>
<Button theme="emphasis">CONNECT</Button>
</NgIf>
<NgIf cond={this.state.type === 'ftp'}>
<Input type="text" name="hostname" placeholder="Hostname*" ref={(input) => {this.state.refs.ftp_hostname = input; }} autoComplete="new-password" />
<Input type="text" name="username" placeholder="Username" ref={(input) => {this.state.refs.ftp_username = input; }} autoComplete="new-password" />
<Input type="password" name="password" placeholder="Password" ref={(input) => {this.state.refs.ftp_password = input; }} autoComplete="new-password" />
<Input type="hidden" name="type" value="ftp"/>
<label>
<input checked={this.state.advanced_ftp} onChange={e => { this.setState({advanced_ftp: e.target.checked}); }} type="checkbox" autoComplete="new-password"/> Advanced
</label>
<NgIf cond={this.state.advanced_ftp === true} className="advanced_form">
<Input type="text" name="path" placeholder="Path" ref={(input) => {this.state.refs.ftp_path = input; }} autoComplete="new-password" />
<Input type="text" name="port" placeholder="Port" ref={(input) => {this.state.refs.ftp_port = input; }} autoComplete="new-password" />
</NgIf>
<Button type="submit" theme="emphasis">CONNECT</Button>
</NgIf>
<NgIf cond={this.state.type === 'sftp'}>
<Input type="text" name="host" placeholder="Hostname*" ref={(input) => {this.state.refs.sftp_host = input; }} autoComplete="new-password" />
<Input type="text" name="username" placeholder="Username" ref={(input) => {this.state.refs.sftp_username = input; }} autoComplete="new-password" />
<Input type="password" name="password" placeholder="Password" ref={(input) => {this.state.refs.sftp_password = input; }} autoComplete="new-password" />
<Input type="hidden" name="type" value="sftp"/>
<label>
<input checked={this.state.advanced_sftp} onChange={e => { this.setState({advanced_sftp: JSON.parse(e.target.checked)}); }} type="checkbox" autoComplete="new-password"/> Advanced
</label>
<NgIf cond={this.state.advanced_sftp === true} className="advanced_form">
<Input type="text" name="path" placeholder="Path" ref={(input) => {this.state.refs.sftp_path = input; }} autoComplete="new-password" />
<Input type="text" name="port" placeholder="Port" ref={(input) => {this.state.refs.sftp_port = input; }} autoComplete="new-password" />
<Textarea type="text" rows="1" name="private_key" placeholder="Private Key" ref={(input) => {this.state.refs.sftp_private_key = input; }} autoComplete="new-password" />
</NgIf>
<Button type="submit" theme="emphasis">CONNECT</Button>
</NgIf>
<NgIf cond={this.state.type === 'git'}>
<Input type="text" name="repo" placeholder="Repository*" ref={(input) => {this.state.refs.git_repo = input; }} autoComplete="new-password" />
<Textarea type="password" rows="1" name="password" placeholder="Password" ref={(input) => {this.state.refs.git_password = input; }} autoComplete="new-password" />
<Input type="hidden" name="type" value="git"/>
<label>
<input checked={this.state.advanced_git} onChange={e => { this.setState({advanced_git: JSON.parse(e.target.checked)}); }} type="checkbox" autoComplete="new-password"/> Advanced
</label>
<NgIf cond={this.state.advanced_git === true} className="advanced_form">
<Input type="text" name="username" placeholder="Username" ref={(input) => {this.state.refs.git_username = input; }} autoComplete="new-password" />
<Input type="text" name="passphrase" placeholder="Passphrase" ref={(input) => {this.state.refs.git_passphrase = input; }} autoComplete="new-password" />
<Input type="text" name="commit" placeholder="Commit Format: default to '{action}({filename}): {path}'" ref={(input) => {this.state.refs.git_commit = input; }} autoComplete="new-password" />
<Input type="text" name="branch" placeholder="Branch: default to 'master'" ref={(input) => {this.state.refs.git_branch = input; }} autoComplete="new-password" />
<Input type="text" name="author_email" placeholder="Author email" ref={(input) => {this.state.refs.git_author_email = input; }} autoComplete="new-password" />
<Input type="text" name="author_name" placeholder="Author name" ref={(input) => {this.state.refs.git_author_name = input; }} autoComplete="new-password" />
<Input type="text" name="committer_email" placeholder="Committer email" ref={(input) => {this.state.refs.git_committer_email = input; }} autoComplete="new-password" />
<Input type="text" name="committer_name" placeholder="Committer name" ref={(input) => {this.state.refs.git_committer_name = input; }} autoComplete="new-password" />
</NgIf>
<Button type="submit" theme="emphasis">CONNECT</Button>
</NgIf>
<NgIf cond={this.state.type === 's3'}>
<Input type="text" name="access_key_id" placeholder="Access Key ID*" ref={(input) => {this.state.refs.s3_access_key_id = input; }} autoComplete="new-password" />
<Input type="password" name="secret_access_key" placeholder="Secret Access Key*" ref={(input) => {this.state.refs.s3_secret_access_key = input; }} autoComplete="new-password" />
<Input type="hidden" name="type" value="s3"/>
<label>
<input checked={this.state.advanced_s3} onChange={e => { this.setState({advanced_s3: JSON.parse(e.target.checked)}); }} type="checkbox" autoComplete="new-password"/> Advanced
</label>
<NgIf cond={this.state.advanced_s3 === true} className="advanced_form">
<Input type="text" name="path" placeholder="Path" ref={(input) => {this.state.refs.s3_path = input; }} autoComplete="new-password" />
</NgIf>
<Button type="submit" theme="emphasis">CONNECT</Button>
</NgIf>
<NgIf cond={this.state.type === 'dropbox'} className="third-party">
<a target="_blank" href={this.state.dropbox_url}>
<div onClick={this.onSubmit.bind(this)}>
<img src="/img/dropbox.png"/>
</div>
<Input type="hidden" name="type" value="dropbox"/>
<Button type="submit" theme="emphasis">LOGIN WITH DROPBOX</Button>
</a>
</NgIf>
<NgIf cond={this.state.type === 'gdrive'} className="third-party">
<div onClick={this.onSubmit.bind(this)}>
<img src="/img/google-drive.png"/>
</div>
<Input type="hidden" name="type" value="gdrive"/>
<Button type="submit" theme="emphasis">LOGIN WITH GOOGLE</Button>
</NgIf>
</form>
</div>
</Card>
);
}
}

View file

@ -0,0 +1,44 @@
.component_page_connection_form{
border-radius: 3px;
box-shadow: 1px 1px 2px rgba(0,0,0,0.2);
div.buttons{
display: flex;
margin: -10px -11px 20px;
padding: 0px 0px 6px 0;
button{
min-width: 80px;
border-top-left-radius: 0;
border-top-right-radius: 0;
padding: 8px 5px;
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
&.active{
transition: box-shadow 0.2s;
box-shadow: 0px 2px 20px rgba(0,0,0,0.2);
}
}
}
form{
label{
color: rgba(0,0,0,0.4);
font-style: italic;
font-size: 0.9em;
display: block;
}
.advanced_form{
max-height: 156px;
overflow-y: auto;
margin-top: 5px
}
button.emphasis{
margin-top: 15px;
color: white;
}
.third-party{
text-align: center;
img{
height: 115px;
margin: 0;
}
}
}
}

View file

@ -0,0 +1,4 @@
export { ForkMe } from './forkme';
export { RememberMe } from './rememberme';
export { Form } from './form';
export { Credentials } from './credentials';

View file

@ -0,0 +1,10 @@
import React from 'react';
import './rememberme.scss';
export const RememberMe = (props) => {
return (
<label className="no-select component_rememberme">
<input checked={props.state} onChange={(e) => props.onChange(e.target.checked)} type="checkbox"/> Remember me
</label>
);
};

View file

@ -0,0 +1,8 @@
.component_rememberme{
color: rgba(0,0,0,0.4);
font-style: italic;
font-size: 0.9em;
display: inline-block;
width: 100%;
text-align: right;
}

View file

@ -15,7 +15,7 @@ export class LogoutPage extends React.Component {
})
.catch((res) => {
console.warn(res)
})
});
}
render() {
return (

View file

@ -1,38 +1,24 @@
import React from 'react'
import React from 'react';
import PropTypes from 'prop-types';
import { theme } from './theme';
import './buttons.scss';
export class Button extends React.Component {
constructor(props){
super(props);
}
style(){
let style = {};
style.border = 'none';
style.margin = '0';
style.padding = '5px';
style.width = '100%';
style.display = 'inline-block';
style.outline = 'none';
style.cursor = 'pointer';
style.fontSize = 'inherit';
style.borderRadius = '2px';
style.color = 'inherit';
if(this.props.theme === 'primary'){ style.background = theme.colors.primary; style.color = 'white'}
else if(this.props.theme === 'secondary'){ style.background = theme.colors.secondary; style.color = 'white'}
else if(this.props.theme === 'emphasis'){ style.background = theme.colors.emphasis; style.color = 'white'}
else{style.background = 'inherit'}
return Object.assign(style, this.props.style);
}
render() {
let props = Object.assign({}, this.props);
delete props.theme;
return (
<button onClick={this.props.onClick} style={this.style()}>{this.props.children}</button>
<button {...props} className={this.props.theme || '' +" "+this.props.className || ''}>
{this.props.children}
</button>
);
}
}
Button.propTypes = {
theme: PropTypes.string
};

View file

@ -0,0 +1,26 @@
button{
border: none;
margin: 0;
padding: 5px;
width: 100%;
display: inline-block;
outline: none;
cursor: pointer;
font-size: inherit;
border-radius: 2px;
color: inherit;
background: inherit;
&.primary{
background: var(--primary);
color: white;
}
&.secondary{
background: var(--secondary);
color: white;
}
&.emphasis{
background: var(--emphasis);
color: white
}
}

View file

@ -1,12 +1,14 @@
import React from 'react';
import {theme} from './theme';
import './card.scss';
export class Card extends React.Component {
constructor(props){
super(props);
this.state = {
dragging: false
}
};
}
onClick(){
@ -28,20 +30,8 @@ export class Card extends React.Component {
}
render() {
let style = {};
style.padding = '10px';
style.color = '#474747';
style.cursor = 'pointer';
style.margin = '2px 0';
style.background = 'white'; style.boxShadow = theme.effects.shadow_large;
style.overflow = 'hidden';
style.position = 'relative';
for(let key in this.props.style){
style[key] = this.props.style[key];
}
return (
<div onClick={this.onClick.bind(this)} onMouseEnter={this.onMouseEnter.bind(this)} onMouseLeave={this.onMouseLeave.bind(this)} style={style}>
<div {...this.props} className={this.props.className+" box"}>
{this.props.children}
</div>
);

View file

@ -0,0 +1,9 @@
.box{
padding: 10px;
cursor: pointer;
margin: 2px 0;
background: white;
box-shadow: rgba(158, 163, 172, 0.3) 0px 19px 60px, rgba(158, 163, 172, 0.22) 0px 15px 20px;
overflow: hidden;
position: relative;
}

View file

@ -1,14 +1,16 @@
import React from 'react'
import React from 'react';
import PropTypes from 'prop-types';
import './container.scss';
export class Container extends React.Component {
constructor(props){
super(props);
}
render() {
const style = Object.assign({width: '95%', maxWidth: this.props.maxWidth || '800px', margin: '0 auto', padding: '10px'}, this.props.style || {});
const style = this.props.maxWidth ? {maxWidth: this.props.maxWidth} : {};
return (
<div style={style}>
<div className="component_container" style={style}>
{this.props.children}
</div>
);

View file

@ -0,0 +1,6 @@
.component_container{
width: 95%;
max-width: 800px;
margin: 0 auto;
padding: 10px;
}

View file

@ -3,15 +3,11 @@ const algorithm = 'aes-256-ctr';
export function encrypt(obj, key){
const cipher = crypto.createCipher(algorithm, key);
return cipher.update(JSON.stringify(obj), 'utf8', 'hex') + cipher.final('hex');
return cipher.update(JSON.stringify(obj), 'utf8', 'base64') + cipher.final('base64');
}
export function decrypt(text, key){
var decipher = crypto.createDecipher(algorithm, key)
try{
return JSON.parse(decipher.update(text,'hex','utf8') + decipher.final('utf8'));
}catch(err){
return {}
}
return JSON.parse(decipher.update(text,'base64','utf8') + decipher.final('utf8'));
}

View file

@ -1,22 +1,10 @@
import React from 'react';
import { theme } from './theme';
import './fab.scss';
export const Fab = (props) => {
let style = {};
style.height = '25px';
style.width = '25px';
style.padding = '13px';
style.position = 'fixed';
style.bottom = '20px';
style.right = '20px';
style.borderRadius = '50%';
style.background = theme.colors.text;
style.boxShadow = theme.effects.shadow;
style.zIndex = '1000';
style.cursor = 'pointer';
return (
<div onClick={props.onClick} style={style}>
<div className="component_fab" onClick={props.onClick}>
{props.children}
</div>
)
);
}

13
client/utilities/fab.scss Normal file
View file

@ -0,0 +1,13 @@
.component_fab{
height: 25px;
width: 25px;
padding: 13px;
position: fixed;
bottom: 20px;
right: 20px;
border-radius: 50%;
background: var(--color);
box-shadow: rgba(0, 0, 0, 0.14) 0px 4px 5px 0px, rgba(0, 0, 0, 0.12) 0px 1px 10px 0px, rgba(0, 0, 0, 0.2) 0px 2px 4px -1px;
z-index: 1000;
cursor: pointer;
}

View file

@ -1,4 +1,5 @@
import React from 'react';
import './icon.scss';
export const Icon = (props) => {
let url;
@ -35,10 +36,8 @@ export const Icon = (props) => {
}else{
throw('unknown icon');
}
let style = props.style || {};
style.verticalAlign = 'bottom';
style.maxHeight = '100%';
return (
<img onClick={props.onClick} src={url} alt={props.name} style={style}/>
<img className="component_icon" style={props.style} onClick={props.onClick} src={url} alt={props.name}/>
);
}

View file

@ -0,0 +1,4 @@
.component_icon{
vertical-align: bottom;
max-height: 100%;
}

View file

@ -1,8 +1,8 @@
export { Input } from './input';
export { Textarea } from './textarea';
export { Button } from './button';
export { Modal } from './modal';
export { Container } from './container';
export { LoremIpsum } from './loremipsum';
export { NgIf } from './ngif';
export { Card } from './card';
export { URL_HOME, goToHome, URL_FILES, goToFiles, URL_VIEWER, goToViewer, URL_LOGIN, goToLogin, URL_LOGOUT, goToLogout } from './navigate';
@ -17,3 +17,4 @@ export { theme, to_rgba } from './theme';
export { encrypt, decrypt } from './crypto'
export { pathBuilder } from './path';
export { Bundle } from './bundle';
export { memory } from './memory';

View file

@ -1,38 +1,18 @@
import React from 'react'
import React from 'react';
import PropTypes from 'prop-types';
import { theme } from './theme';
import './input.scss';
export class Input extends React.Component {
constructor(props){
super(props);
}
style(){
let style = this.props.style || {};
style.background = 'inherit';
style.border = 'none';
style.borderRadius = '0';
style.borderBottom = '2px solid rgba(70, 99, 114, 0.1)'
style.width = '100%';
style.display = 'inline-block';
style.fontSize = 'inherit';
style.padding = '5px 0px 5px 0px';
style.margin = '0 0 8px 0';
style.outline = 'none';
style.boxSizing = 'border-box';
style.color = 'inherit';
return style;
}
render() {
return (
<input
style={this.style()}
name={this.props.name}
type={this.props.type}
value={this.props.value}
defaultValue={this.props.defaultValue}
placeholder={this.props.placeholder || ''}
className="component_input"
{...this.props}
ref={(comp) => { this.ref = comp; }}
/>
);
}

View file

@ -0,0 +1,14 @@
.component_input{
background: inherit;
border: none;
border-radius: 0;
border-bottom: 2px solid rgba(70, 99, 114, 0.1);
width: 100%;
display: inline-block;
font-size: inherit;
padding: 5px 0px 5px 0px;
margin: 0 0 8px 0;
outline: none;
box-sizing: border-box;
color: inherit;
}

View file

@ -1,11 +1,9 @@
import React from 'react';
import './loader.scss';
export const Loader = (props) => {
let style = props.style || {};
style.textAlign = 'center';
style.marginTop = '50px';
return (
<div style={style}>
<div className="component_loader">
<svg width="120px" height="120px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<rect x="0" y="0" width="100" height="100" fill="none"></rect>
<circle cx="50" cy="50" r="40" stroke="rgba(100%,100%,100%,0.679)" fill="none" strokeWidth="10" strokeLinecap="round"></circle>
@ -16,4 +14,4 @@ export const Loader = (props) => {
</svg>
</div>
);
}
};

View file

@ -0,0 +1,4 @@
.component_loader{
text-align: center;
margin-top: 50px;
}

View file

@ -1,16 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types';
export class LoremIpsum extends React.Component {
constructor(props){
super(props);
}
render() {
return (
<div>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
</div>
);
}
}

View file

@ -0,0 +1,17 @@
function Memory(){
let data = {};
return {
get: function(key){
if(!data[key]) return null;
return data[key];
},
set: function(key, value){
if(!data[key]) data[key] = {};
data[key] = value;
}
}
}
export const memory = new Memory();

62
client/utilities/modal.js Normal file
View file

@ -0,0 +1,62 @@
import React from 'react';
import PropTypes from 'prop-types';
import { theme } from './theme';
import { Input } from './input';
import { Button } from './button';
import { NgIf } from './ngif';
import './modal.scss';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
export class Modal extends React.Component {
constructor(props){
super(props);
this.state = {
marginTop: this._marginTop()
};
}
onClick(e){
if(e.target.getAttribute('id') === 'modal-box'){
this.props.onQuit && this.props.onQuit();
}
}
componentDidMount(){
this.setState({marginTop: this._marginTop()});
}
_marginTop(){
let size = 300;
const $box = document.querySelector('#modal-box > div');
if($box) size = $box.offsetHeight;
size = Math.round((document.body.offsetHeight - size) / 2);
if(size < 0) return 0;
if(size > 250) return 250;
return size;
}
render() {
return (
<ReactCSSTransitionGroup transitionName="modal"
transitionLeaveTimeout={300}
transitionEnterTimeout={300}
transitionAppear={true} transitionAppearTimeout={300}
>
<NgIf key={"modal-"+this.props.isActive} cond={this.props.isActive}>
<div className="component_modal" onClick={this.onClick.bind(this)} id="modal-box">
<div key="random" style={{margin: this.state.marginTop+'px auto 0 auto'}}>
{this.props.children}
</div>
</div>
</NgIf>
</ReactCSSTransitionGroup>
);
}
}
Modal.propTypes = {
isActive: PropTypes.bool.isRequired,
onQuit: PropTypes.func
};

View file

@ -0,0 +1,55 @@
.component_modal{
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.7);
box-shadow: 1px 2px 10px rgba(0,0,0,0.2);
> div{
background: white;
width: 65%;
max-width: 300px;
padding: 40px 20px 0 20px;
border-radius: 2px;
}
}
/***********************/
/* ENTERING TRANSITION */
// background
.modal-appear > div, .modal-enter > div{ opacity: 0;}
.modal-appear.modal-appear-active > div, .modal-enter.modal-enter-active > div{
opacity: 1;
transition: opacity 0.2s ease;
}
// box
.modal-appear > div > div, .modal-enter > div > div{
opacity: 0;
transform: translateY(10px);
}
.modal-appear.modal-appear-active > div > div, .modal-enter.modal-enter-active > div > div{
opacity: 1;
transform: translateY(0);
transition: all 0.2s ease-out;
transition-delay: 0.1s;
}
/***********************/
/* LEAVING TRANSITION */
// background
.modal-leave > div{ opacity: 1; }
.modal-leave.modal-leave-active > div{
opacity: 0;
transition: opacity 0.2s ease-out;
transition-delay: 0.1s;
}
// box
.modal-leave > div > div { opacity: 1; }
.modal-leave.modal-leave-active > div > div{
opacity: 0;
transition: all 0.3s ease;
}

View file

@ -5,10 +5,13 @@ export class NgIf extends React.Component {
constructor(props){
super(props);
}
render() {
let clean_prop = Object.assign({}, this.props);
delete clean_prop.cond;
delete clean_prop.children;
if(this.props.cond){
return <div onClick={this.props.onClick} style={this.props.style}>{this.props.children}</div>;
return <div {...clean_prop}>{this.props.children}</div>;
}else{
return null;
}

View file

@ -1,7 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NgIf } from './';
import { theme } from './theme';
import { theme } from './theme';
import './notification.scss';
export class Notification extends React.Component {
constructor(props){
@ -10,7 +12,7 @@ export class Notification extends React.Component {
visible: null,
error: null,
timeout: null
}
};
}
componentWillMount(){
@ -38,26 +40,27 @@ export class Notification extends React.Component {
formatError(err){
if(typeof err === 'object'){
if(err && err.message){
return err.message
return err.message;
}else{
return JSON.stringify(err);
}
}else if(typeof err === 'string'){
return err;
}else{
throw('unrecognized notification')
throw('unrecognized notification');
}
}
render(){
return (
<NgIf cond={this.state.visible === true} style={{position: 'fixed', bottom: 0, left: 0, right: 0, textAlign: 'center'}}>
<div onClick={this.toggleVisibility.bind(this)} style={{display: 'inline-block', background: '#637d8b', minWidth: '200px', maxWidth: '400px', margin: '0 auto', padding: '10px 15px', borderTopLeftRadius: '3px', borderTopRightRadius: '3px', color: 'white', textAlign: 'left', cursor: 'pointer', boxShadow: theme.effects.shadow}}>
{this.formatError(this.state.error)}
<NgIf cond={this.state.visible === true}>
<div className="component_notification">
<div onClick={this.toggleVisibility.bind(this)}>
{this.formatError(this.state.error)}
</div>
</div>
</NgIf>
)
);
}
}

View file

@ -0,0 +1,22 @@
.component_notification{
position: fixed;
bottom: 0;
left: 0;
right: 0;
text-align: center;
> div{
display: inline-block;
background: #637d8b;
min-width: 200px;
max-width: 400px;
margin: 0 auto;
padding: 10px 15px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
color: white;
text-align: left;
cursor: pointer;
box-shadow: rgba(0, 0, 0, 0.14) 0px 4px 5px 0px, rgba(0, 0, 0, 0.12) 0px 1px 10px 0px, rgba(0, 0, 0, 0.2) 0px 2px 4px -1px;
}
}

View file

@ -1,38 +1,20 @@
import React from 'react'
import React from 'react';
import PropTypes from 'prop-types';
import { theme } from './theme';
import './textarea.scss';
export class Textarea extends React.Component {
constructor(props){
super(props);
}
style(){
let style = this.props.style || {};
style.background = 'inherit';
style.border = 'none';
style.borderRadius = '0';
style.borderBottom = '2px solid rgba(70, 99, 114, 0.1)'
style.width = '100%';
style.display = 'inline-block';
style.fontSize = 'inherit';
style.padding = '5px 0px 5px 0px';
style.margin = '0 0 8px 0';
style.outline = 'none';
style.boxSizing = 'border-box';
style.color = 'inherit';
return style;
}
render() {
return (
<textarea
style={this.style()}
name={this.props.name}
type={this.props.type}
value={this.props.value}
defaultValue={this.props.defaultValue}
placeholder={this.props.placeholder || ''}
{...this.props}
className='component_textarea'
ref={(comp) => { this.ref = comp; }}
></textarea>
);
}

View file

@ -0,0 +1,15 @@
.component_textarea{
background: inherit;
border: none;
border-radius: 0;
border-bottom: 2px solid rgba(70, 99, 114, 0.1);
width: 100%;
display: inline-block;
font-size: inherit;
font-family: inherit;
padding: 5px 0px 5px 0px;
margin: 0 0 8px 0;
outline: none;
box-sizing: border-box;
color: inherit;
}

View file

@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
import { DropTarget, DragSource } from 'react-dnd';
import { NgIf } from './';
import './uploader.scss';
const FileTarget = {
drop(props, monitor) {
props.onUpload(props.path, monitor.getItem().files);
@ -24,18 +26,12 @@ export class Uploader extends React.Component {
}
render(){
const style = {
position: 'absolute', top: 0, bottom: 0, left: 0, right: 0,
background: 'rgba(0,0,0,0.2)',
padding: '50% 0',
textAlign: 'center'
}
return this.props.connectDropTarget(
<div>
<NgIf cond={this.props.isOver && this.props.canDrop} style={style}>
<NgIf cond={this.props.isOver && this.props.canDrop}>
DRAG FILE HERE
</NgIf>
<div>
<div className="component_uploader">
{this.props.children}
</div>
</div>

View file

@ -0,0 +1,12 @@
/*
.component_uploader{
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.2);
padding: 50% 0;
text-align: center;
}
*/

View file

@ -5,9 +5,10 @@
// DROPBOX
// 1) create an third party app: https://www.dropbox.com/developers/apps/create
// -> dropbox api -> Full Dropbox -> whatever name you want ->
// -> set redirect URI to https://example.com/login ->
// -> set redirect URI to https://example.com/login ->
module.exports = {
// SERVER CONFIG
info: {
host: "application_url",
usage_stats: true
@ -20,5 +21,7 @@ module.exports = {
dropbox: {
clientID: "dropbox_client_id",
redirectURI: "application_url/login"
}
},
// APPLICATION CONFIG
fork_button: true
}

1788
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -38,6 +38,7 @@
"pdfjs-dist": "^1.8.426",
"prop-types": "^15.5.10",
"react": "^15.3.2",
"react-addons-css-transition-group": "^15.6.2",
"react-dnd": "^2.4.0",
"react-dnd-html5-backend": "^2.4.1",
"react-dnd-touch-backend": "^0.3.11",
@ -68,10 +69,16 @@
"babel-preset-es2015": "^6.13.2",
"babel-preset-react": "^6.11.1",
"babel-preset-stage-2": "^6.24.1",
"css-loader": "^0.28.10",
"html-loader": "^0.4.5",
"html-webpack-plugin": "^2.28.0",
"http-server": "^0.9.0",
"less-loader": "^4.0.6",
"node-sass": "^4.7.2",
"nodemon": "^1.11.0",
"sass-loader": "^6.0.6",
"sass-variable-loader": "^0.1.2",
"style-loader": "^0.20.2",
"webpack-bundle-analyzer": "^2.8.2",
"webpack-dev-server": "^2.4.5"
}

View file

@ -1,14 +1,30 @@
:root {
--bg-color: #f2f2f2;
--color: #626469;
--emphasis: #375160;
--primary: #9AD1ED;
--emphasis-primary: #2b71bc;
--secondary: #466372;
--emphasis-secondary: #466372;
--super-light: #ecf1f6;
--error: #f26d6d;
--success: #63d9b1;
}
html {
font-family:"San Francisco","Roboto","Arial",sans-serif;
-webkit-text-size-adjust:100%;
background: #f2f2f2;
color: #6f6f6f;
background: var(--bg-color);
color: var(--color);
}
body, html{
height: 100%;
margin: 0;
}
a{color: inherit; text-decoration: none;}
.scroll-y{
@ -32,6 +48,16 @@ select::-ms-expand {
display: none;
}
button::-moz-focus-inner {
border: 0;
}
input, textarea{
transition: border 0.2s;
}
input[type="checkbox"]{position: relative; top: 1px; margin: 0; padding: 0;}
.no-select {
-webkit-touch-callout: none;
-webkit-user-select: none;
@ -42,6 +68,19 @@ select::-ms-expand {
}
.connect-form input:hover, .connect-form textarea:hover,
.connect-form input:focus, .connect-form textarea:focus{
border-color: rgb(154, 209, 237)!important;
}
.drag-drop{
z-index: 2;
}
@ -52,5 +91,43 @@ select::-ms-expand {
/* I know this looks weird but at least it doesn't rely on javascript to dynamically set element size */
body {overflow: hidden;}
body, body > div, body > div > div, body > div > div > div{ height: 100%;}
/* CONNECTION FORM */
.login-form button.active{
box-shadow: 0px 1px 5px rgba(0,0,0,0.20);
}
/* ANIMATION */
.example-enter {
opacity: 0.01;
}
.example-enter.example-enter-active {
opacity: 1;
transition: opacity 500ms ease-in;
}
.example-leave {
opacity: 1;
}
.example-leave.example-leave-active {
opacity: 0.01;
transition: opacity 300ms ease-in;
}

View file

@ -15,15 +15,19 @@ let config = {
chunkFilename: "js/chunk.[name].[id].js"
},
module: {
loaders: [
rules: [
{
test: path.join(__dirname, 'client'),
loader: ['babel-loader'],
use: ['babel-loader'],
exclude: /node_modules/
},
{
test: /\.html$/,
loader: 'html-loader'
},
{
test: /\.scss$/,
loaders: ['style-loader', 'css-loader', 'sass-loader']
}
]
},
@ -59,10 +63,6 @@ if(process.env.NODE_ENV === 'production'){
}
}
};
//config.entry.push('webpack/hot/only-dev-server');
}
//config.plugins.push(new BundleAnalyzerPlugin())
module.exports = config;