summaryrefslogtreecommitdiff
path: root/web-ui
diff options
context:
space:
mode:
Diffstat (limited to 'web-ui')
-rw-r--r--web-ui/package.json23
-rw-r--r--web-ui/public/dummy.json1
-rw-r--r--web-ui/public/images/pixelated-logo-orange.svg29
-rw-r--r--web-ui/public/images/sent_email.pngbin0 -> 9160 bytes
-rw-r--r--web-ui/public/signup.css174
-rw-r--r--web-ui/public/signup.html19
-rw-r--r--web-ui/src/js/index.js235
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.png
new file mode 100644
index 00000000..ddaa11d0
--- /dev/null
+++ b/web-ui/public/images/sent_email.png
Binary files differ
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')
+);