diff options
Diffstat (limited to 'web-ui')
| -rw-r--r-- | web-ui/package.json | 23 | ||||
| -rw-r--r-- | web-ui/public/dummy.json | 1 | ||||
| -rw-r--r-- | web-ui/public/images/pixelated-logo-orange.svg | 29 | ||||
| -rw-r--r-- | web-ui/public/images/sent_email.png | bin | 0 -> 9160 bytes | |||
| -rw-r--r-- | web-ui/public/signup.css | 174 | ||||
| -rw-r--r-- | web-ui/public/signup.html | 19 | ||||
| -rw-r--r-- | web-ui/src/js/index.js | 235 | 
7 files changed, 479 insertions, 2 deletions
| diff --git a/web-ui/package.json b/web-ui/package.json index 2a0056e4..b937502f 100644 --- a/web-ui/package.json +++ b/web-ui/package.json @@ -5,20 +5,23 @@    "repository": "https://github.com/pixelated-project/pixelated-user-agent",    "private": true,    "devDependencies": { +    "babel": "^6.5.2", +    "babel-cli": "^6.18.0",      "bower": "1.7.9", +    "browserify": "^13.1.1",      "handlebars": "4.0.5",      "html-minifier": "2.1.6",      "imagemin": "5.2.1",      "jshint": "2.9.2",      "karma": "0.13.19",      "karma-chrome-launcher": "0.2.2", +    "karma-coverage": "0.2.7",      "karma-firefox-launcher": "0.1.7",      "karma-jasmine": "0.2.2",      "karma-jasmine-ajax": "0.1.13",      "karma-junit-reporter": "0.2.2",      "karma-phantomjs-launcher": "1.0.1",      "karma-requirejs": "1.0.0", -    "karma-coverage": "0.2.7",      "minify": "2.0.9",      "requirejs": "2.2.0",      "watch": "0.19.1" @@ -32,7 +35,8 @@      "handlebars-watch": "node_modules/.bin/watch 'npm run handlebars' app/templates",      "compass": "compass compile",      "compass-watch": "compass watch", -    "build": "npm run clean && npm run handlebars && npm run add_git_version && npm run compass", +    "build": "npm run clean && npm run handlebars && npm run add_git_version && npm run compass && npm run build-signup", +    "build-signup": "babel src/js -d lib/js && browserify lib/js/index.js >public/signup.js",      "jshint": "node_modules/jshint/bin/jshint --config=.jshintrc app test",      "clean": "rm -rf .tmp/ 'dist/*' app/js/generated/hbs/* app/css/*",      "buildmain": "node_modules/requirejs/bin/r.js -o config/buildoptions.js", @@ -41,5 +45,20 @@      "minify_html": "node_modules/.bin/html-minifier app/index.html --collapse-whitespace | sed 's|<!--usemin_start-->.*<!--usemin_end-->|<script src=\"assets/app.min.js\" type=\"text/javascript\"></script>|' > dist/index.html",      "minify_sandbox": "node_modules/.bin/html-minifier app/sandbox.html --collapse-whitespace | sed 's|<!--usemin_start-->.*<!--usemin_end-->|<script src=\"sandbox.min.js\" type=\"text/javascript\"></script>|' > dist/sandbox.html",      "add_git_version": "/bin/bash config/add_git_version.sh" +  }, +  "dependencies": { +    "babel-preset-es2015": "^6.18.0", +    "babel-preset-react": "^6.16.0", +    "immutable": "^3.8.1", +    "react": "^15.3.2", +    "react-dom": "^15.3.2", +    "redux": "^3.6.0", +    "whatwg-fetch": "^2.0.0" +  }, +  "babel": { +    "presets": [ +      "es2015", +      "react" +    ]    }  } diff --git a/web-ui/public/dummy.json b/web-ui/public/dummy.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/web-ui/public/dummy.json @@ -0,0 +1 @@ +{} diff --git a/web-ui/public/images/pixelated-logo-orange.svg b/web-ui/public/images/pixelated-logo-orange.svg new file mode 100644 index 00000000..7e0ef43d --- /dev/null +++ b/web-ui/public/images/pixelated-logo-orange.svg @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0)  --> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" +	 width="509.707px" height="142.439px" viewBox="0 0 509.707 142.439" enable-background="new 0 0 509.707 142.439" +	 xml:space="preserve"> +<g> +	<path fill="#F9A731" d="M0,35.469v71.365l62.837,35.605l62.833-35.605V35.469L62.813,0L0,35.469z M60.262,116.617L23.735,96.332 +		V52.46l36.586,20.999L60.262,116.617z M101.936,96.332l-36.148,20.285l0.067-43.123l36.081-21.034V96.332z M101.936,46.44 +		L62.951,69.553L23.733,46.44l39.218-21.131L101.936,46.44z"/> +	<path fill="#F9A731" d="M169.505,42.332h-19.968v59.328h13.52V79.655h6.448c11.579,0,20.279-6.832,20.279-19.056 +		C189.784,48.302,181.084,42.332,169.505,42.332z M166.866,68.868h-3.809v-15.75h3.809c5.323,0,10.357,1.798,10.357,7.91 +		C177.224,67.07,172.189,68.868,166.866,68.868z"/> +	<rect x="194.309" y="42.332" fill="#F9A731" width="13.52" height="59.328"/> +	<polygon fill="#F9A731" points="266.516,42.332 249.689,42.332 238.759,58.514 227.827,42.332 211.721,42.332 230.417,69.73  +		210.228,101.66 226.982,101.66 238.759,81.453 250.534,101.66 268.01,101.66 247.099,69.73 	"/> +	<polygon fill="#F9A731" points="270.128,101.66 304.069,101.66 304.069,89.795 283.647,89.795 283.647,77.857 303.207,77.857  +		303.207,65.991 283.647,65.991 283.647,54.199 304.069,54.199 304.069,42.332 270.128,42.332 	"/> +	<path fill="#F9A731" d="M354.807,42.332l-19.156,47.463H322.33V42.332h-13.52v59.328h22.053h11.888h2.636l4.386-11.865h22.578 +		l4.391,11.865h14.524l-23.944-59.328H354.807z M354.377,77.928l6.614-17.257h0.145l6.615,17.257H354.377z"/> +	<polygon fill="#F9A731" points="379.939,54.199 394.073,54.199 394.073,101.66 407.592,101.66 407.592,54.199 421.687,54.199  +		421.687,42.332 379.939,42.332 	"/> +	<polygon fill="#F9A731" points="426.265,101.66 460.207,101.66 460.207,89.795 439.785,89.795 439.785,77.857 459.344,77.857  +		459.344,65.991 439.785,65.991 439.785,54.199 460.207,54.199 460.207,42.332 426.265,42.332 	"/> +	<path fill="#F9A731" d="M479.792,42.332h-14.94v59.328h14.94c16.324,0,29.914-12.37,29.914-29.699 +		C509.707,54.701,496.044,42.332,479.792,42.332z M480.457,89.577h-2.084V54.414h2.084c10.067,0,16.9,7.695,16.9,17.619 +		C497.285,81.955,490.455,89.577,480.457,89.577z"/> +</g> +</svg> diff --git a/web-ui/public/images/sent_email.png b/web-ui/public/images/sent_email.pngBinary files differ new file mode 100644 index 00000000..ddaa11d0 --- /dev/null +++ b/web-ui/public/images/sent_email.png diff --git a/web-ui/public/signup.css b/web-ui/public/signup.css new file mode 100644 index 00000000..61ac8587 --- /dev/null +++ b/web-ui/public/signup.css @@ -0,0 +1,174 @@ +body { +  font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif; +} + +.field-group { +  position:relative; +  margin-bottom: 35px; +} + +label { +  font-size: 0.9em; +  margin-bottom: 10px; +  display: inline-block; +} + +input { +  display: block; +  border: solid 1px #4da3b6; +  width: 100%; +  height: auto; +  padding: 10px 5px; +  margin-bottom: 20px; +} + +.animated-label { +  color:#999; +  position:absolute; +  pointer-events:none; +  left: 6px; +  top:10px; +  transition:0.2s ease all; +  -moz-transition:0.2s ease all; +  -webkit-transition:0.2s ease all; +} + +input:focus { +  outline:none; +} + +input:focus ~ .animated-label, input:valid ~ .animated-label{ +  top:-20px; +  left: 0; +  font-size:0.8em; +  color:#4da3b6; +} + +.blue-button { +  background: #178ca6; +  color: white; +  display: block; +  text-decoration: none; +  text-align: center; +  padding: 10px 0 10px 0; +  width: 104%; +  margin: 0 auto; +} + +.blue-button:hover { +  background: #4da3b6; +} + +a { +  text-decoration: none; +  color: #4da3b6; +} + +h1 { +  font-size: 1.5em; +  text-align: center; +} + +header { +  width: 18%; +  margin: 0 auto; +} + +.link-message { +  text-align: center; +  font-size: 0.8em; +} + +.logo { +  width: 100%; +  height: auto; +  padding-top: 20%; +  margin-bottom: 30px; +} + +.message h1 { +    margin-bottom: 35px; +} + +.message p { +  padding-left: 5%; +  padding-right: 5%; +  width: 40%; +  margin: 0 auto; +  text-align: center; +  line-height: 1.8em; +  font-size: 0.9em; +} + +.form-container { +  width: 20%; +  margin: 0 auto; +  padding-top: 40px; +} + +.domain-label { +  position: relative; +  top: 26px; +  padding-left: 20px; +  left: 100%; +} + +.sent-email-icon { +  width: 60px; +} + +.disabled { +  pointer-events: none; +  background: #d4d4d4; +} + +.link-message .disabled { +  pointer-events: none; +  color: #d4d4d4; +  background: none; +} + +/* Medium Devices, Desktops */ +@media only screen and (max-width : 992px) { +  header { +    width: 20%; +  } + +  .form-container { +    width: 30%; +  } + +  .message p { +    width: 70% +  } +} + +/* Small Devices, Tablets */ +@media only screen and (max-width : 768px) { +  header { +    width: 30%; +  } + +  .form-container { +    width: 50%; +  } + +  .message p { +    width: 80% +  } +} + +/* Extra Small Devices, Phones */ +@media only screen and (max-width : 480px) { +  header { +    width: 60%; +  } + +  .form-container { +    width: 80%; +  } + +  .message p { +    width: 85% +  } +} diff --git a/web-ui/public/signup.html b/web-ui/public/signup.html new file mode 100644 index 00000000..9bc6cdad --- /dev/null +++ b/web-ui/public/signup.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +  <head> +    <meta charset="utf-8"> +    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> +    <title>Pixelated Mail</title> +    <meta name="description" content=""> +    <meta name="viewport" content="width=device-width"> +    <link rel="stylesheet" type="text/css" href="/startup-assets/normalize.min.css" /> +    <link rel="stylesheet" type="text/css" href="/public-assets/signup.css" /> +  </head> +  <body> +    <header><img src="images/pixelated-logo-orange.svg" alt="Pixelated" class="logo"/></header> +    <div class="message"> +      <div id="app"></div> +      <script src="/public-assets/signup.js"></script> +    </div> +  </body> +</html> diff --git a/web-ui/src/js/index.js b/web-ui/src/js/index.js new file mode 100644 index 00000000..57ec42a6 --- /dev/null +++ b/web-ui/src/js/index.js @@ -0,0 +1,235 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import {createStore} from 'redux'; +import {Map} from 'immutable'; +import 'whatwg-fetch'; + + +class PixelatedComponent extends React.Component { +  _updateStateFromStore() { +    this.setState(this.props.store.getState().toJS()); +  } + +  componentWillMount() { +    this.unsubscribe = this.props.store.subscribe(() => this._updateStateFromStore()); +    this._updateStateFromStore(); +  } + +  componentWillUnmount() { +    this.unsubscribe() +  } +} + + +class PixelatedForm extends PixelatedComponent { +  _fetchAndDispatch(url, actionProperties) { +    const immutableActionProperties = new Map(actionProperties); +    this.props.store.dispatch(immutableActionProperties.merge({status: 'STARTED'}).toJS()); +    fetch(url).then((response) => { +      return response.json() +    }).then((json) => { +      setTimeout(() => { +        this.props.store.dispatch(immutableActionProperties.merge({status: 'SUCCESS', json: json}).toJS()); +      }, 3000); +    }).catch((error) => { +      console.error('something went wrong', error); +      this.props.store.dispatch(immutableActionProperties.merge({status: 'ERROR', error: error}).toJS()); +    }); +  } +} + + +class InviteCodeForm extends PixelatedForm { +  render() { +    let className = "blue-button validation-link"; + +    if(!this.state.inviteCodeValidation) { +      className = className + " disabled"; +    } + +    return ( +      <form onSubmit={this._handleClick.bind(this)}> +        <div className="field-group"> +          <input type="text" name="invite-code" className="invite-code" onChange={this._handleInputEmpty.bind(this)} required/> +          <label className="animated-label" htmlFor="invite-code">invite code</label> +        </div> +        <input type="submit" value="Get Started" className={className} /> +      </form> +    ); +  } + +  _handleClick(event) { +    event.stopPropagation(); +    event.preventDefault(); +    this.props.store.dispatch({type: 'SUBMIT_INVITE_CODE', inviteCode: event.target['invite-code'].value}); +  } + +  _handleInputEmpty(event) { +    this.props.store.dispatch({type: 'VALIDATE_INVITE_CODE', inviteCode: event.target.value}); +  } +} + + +class CreateAccountForm extends PixelatedForm { +  render() { +    return ( +      <form onSubmit={this._handleClick.bind(this)}> +        <span className="domain-label"> @domain.com </span> +        <div className="field-group"> +          <input type="text" name="username" className="username" required/> +          <label className="animated-label" htmlFor="username">username</label> +        </div> + +        <div className="field-group"> +          <input type="password" name="password" className="password" required/> +          <label className="animated-label" htmlFor="password">password</label> +        </div> + +        <input type="submit" value="Create my account" className="blue-button validation-link" /> +      </form> +    ); +  } + +  _handleClick(event) { +    event.stopPropagation(); +    event.preventDefault(); +    this.props.store.dispatch({type: 'SUBMIT_CREATE_ACCOUNT', username: event.target['username'].value, password: event.target['password'].value}); +  } +} + + +class BackupEmailForm extends PixelatedForm { +  render() { +    return ( +      <form onSubmit={this._handleClick.bind(this)}> +        <div className="field-group"> +          <input type="text" name="backup-email" required/> +          <label className="animated-label" htmlFor="password">type your backup email</label> +        </div> + +        <input type="submit" value="Send Email" className="blue-button validation-link" /> +        <p className="link-message"> +          <a href="#" className="validation-link">I didn't receive anything. Send the email again</a> +        </p> +      </form> +    ); +  } + +  _handleClick(event) { +    event.stopPropagation(); +    event.preventDefault(); +    this._fetchAndDispatch('dummy.json', {type: 'SUBMIT_BACKUP_EMAIL', backupEmail: event.target['backup-email'].value}); +  } +} + + +class BackupEmailSentForm extends PixelatedForm { +  render() { +    return ( +      <form onSubmit={this._handleClick.bind(this)}> +        {this.state.isFetching || <a href="/" className="blue-button">I received the codes. <br/>Go to my inbox</a>} +        <p className="link-message"> +          <a href="#">I didn't receive anything. Send the email again</a> +        </p> +      </form> +    ); +  } + +  _handleClick(event) { +    event.stopPropagation(); +    event.preventDefault(); +  } +} + + +class SignUp extends PixelatedComponent { +  render() { +    return ( +      <div> +        <div className="message"> +          <h1>{this.state.header}</h1> +          {this.state.icon} +          <p>{this.state.summary}</p> +        </div> +        <div className="form-container"> +          {this._form()} +        </div> +      </div> +    ); +  } + +  _form() { +    switch(this.state.form) { +      case 'invite_code': return <InviteCodeForm store={store} />; +      case 'create_account': return <CreateAccountForm store={store} />; +      case 'backup_email': return <BackupEmailForm store={store} />; +      case 'backup_email_sent': return <BackupEmailSentForm store={store} />; +      default: throw Error('TODO'); +    } +  } +} + + +const initialState = new Map({ +  isFetching: false, +  form: 'invite_code', +  header: 'Welcome', +  icon: null, +  summary: ['Do you have an invite code?', <br key='br1' />, 'Type it below'], +}); + + +const store = createStore((state=initialState, action) => { +  switch (action.type) { +  case 'SUBMIT_INVITE_CODE': +    return state.merge({ +      inviteCode: action.inviteCode, +      form: 'create_account', +      header: 'Create your account', +      summary: 'Choose your username, and be careful about your password, it must be strong and easy to remember. If you have a password manager, we strongly advise you to use one.', +    }); +  case 'SUBMIT_CREATE_ACCOUNT': +    return state.merge({ +      username: action.username, +      password: action.password, +      form: 'backup_email', +      header: 'In case you lose your password...', +      summary: 'Set up a backup email account. You\'ll receive an email with a code so you can recover your account in the future, other will be sent to your account administrator.', +    }); +  case 'SUBMIT_BACKUP_EMAIL': +    switch (action.status) { +    case 'STARTED': +      return state.merge({ +        isFetching: true, +        backupEmail: action.backupEmail, +        form: 'backup_email_sent', +        icon: <p><img key="img1" src="images/sent_email.png" className="sent-email-icon"/></p>, +        summary: 'An email was sent to the email you provided. Check your spam folder, just in case.', +      }); +    case 'SUCCESS': +      return state.merge({ +        isFetching: false, +      }); +    case 'ERROR': +      return state.merge({ +        isFetching: false, +      }); +    default: +      return state; +    } +  case 'SUBMIT_BACKUP_EMAIL_SENT': +    return state.merge({}); +  case 'VALIDATE_INVITE_CODE': +    return state.merge({ +      inviteCodeValidation: Boolean(action.inviteCode) +    }); +  default: +    return state; +  } +}); + + +ReactDOM.render( +  <SignUp store={store}/>, +  document.getElementById('app') +); | 
