Implement login
This commit is contained in:
parent
01ed5b6109
commit
738fc96359
@ -1,6 +1,11 @@
|
||||
@import "themes/form/foundation";
|
||||
@import "themes/form/labels/linus";
|
||||
@import "themes/form/validation/linus";
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.timer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -10,3 +15,25 @@ h1 {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
padding: 40px;
|
||||
border: 1px solid black;
|
||||
h1:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
z-index: 9999;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
background: white;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
@import "variables";
|
||||
|
||||
45
assets/less/variables.less
Normal file
45
assets/less/variables.less
Normal file
@ -0,0 +1,45 @@
|
||||
@base-font-size: 16px;
|
||||
|
||||
@default-float: left;
|
||||
@opposite-direction: right;
|
||||
// @text-direction: left;
|
||||
|
||||
|
||||
@base-color: #671637;
|
||||
@color-1: #00002d;
|
||||
@color-2: #0066c6;
|
||||
@color-3: #0066c6;
|
||||
@color-message: #333;
|
||||
@color-error: @base-color;
|
||||
@color-success: @color-1;
|
||||
|
||||
|
||||
// This needs to be a px value since rem and em-calc use it. It is translated
|
||||
// to a percante unit for html and body font-sizing like so: strip-unit(@base-font-size) / 16 * 100%;
|
||||
@base-line-height: 1.6;
|
||||
@base-line-height-computed: @base-line-height * 1rem;
|
||||
|
||||
@body-font-color: #fff;
|
||||
@body-font-family: 'Open Sans', sans-serif;
|
||||
@body-font-weight: 300;
|
||||
@body-font-style: normal;
|
||||
@body-bg: @color-1;
|
||||
|
||||
@font-smoothing: subpixel-antialiased;
|
||||
|
||||
@input-padding: 14px 12px;
|
||||
@input-font-weight: 300;
|
||||
|
||||
// Colors
|
||||
// @form-valid-color: @base-color;
|
||||
// @form-invalid-color: #ffae00;
|
||||
@form-valid-color: @color-success;
|
||||
@form-invalid-color: @color-error;
|
||||
@form-icon-width: 20px;
|
||||
@form-icon-height: 20px;
|
||||
|
||||
|
||||
//
|
||||
// Custom
|
||||
//
|
||||
|
||||
33
client/actions/ui.js
Normal file
33
client/actions/ui.js
Normal file
@ -0,0 +1,33 @@
|
||||
export const SHOW_LOGIN_MODAL = 'SHOW_LOGIN_MODAL';
|
||||
|
||||
export function showLoginModal() {
|
||||
return {
|
||||
type: SHOW_LOGIN_MODAL,
|
||||
};
|
||||
}
|
||||
|
||||
export const HIDE_LOGIN_MODAL = 'HIDE_LOGIN_MODAL';
|
||||
|
||||
export function hideLoginModal() {
|
||||
return {
|
||||
type: HIDE_LOGIN_MODAL,
|
||||
};
|
||||
}
|
||||
|
||||
export const SHOW_FLAG_FORM = 'SHOW_FLAG_FORM';
|
||||
|
||||
export function showFlagForm(id) {
|
||||
return {
|
||||
type: SHOW_FLAG_FORM,
|
||||
id: id,
|
||||
}
|
||||
}
|
||||
|
||||
export const HIDE_FLAG_FORM = 'HIDE_FLAG_FORM';
|
||||
|
||||
export function hideFlagForm(id) {
|
||||
return {
|
||||
type: HIDE_FLAG_FORM,
|
||||
id: id,
|
||||
}
|
||||
}
|
||||
45
client/actions/user.js
Normal file
45
client/actions/user.js
Normal file
@ -0,0 +1,45 @@
|
||||
import { hideLoginModal } from './ui';
|
||||
|
||||
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
|
||||
|
||||
export const LOGIN_FAILURE = 'LOGIN_FAILURE';
|
||||
|
||||
export function login(credentials) {
|
||||
return (dispatch) => {
|
||||
fetch('/auth/local', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(credentials),
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
dispatch(hideLoginModal());
|
||||
dispatch({
|
||||
type: LOGIN_SUCCESS,
|
||||
payload: json,
|
||||
});
|
||||
})
|
||||
.catch((err) => dispatch({
|
||||
type: LOGIN_FAILURE,
|
||||
err,
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
export const LOGOUT = 'LOGOUT';
|
||||
|
||||
export function logout() {
|
||||
return (dispatch) => {
|
||||
fetch('/auth/logout', {
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(() => dispatch({
|
||||
type: LOGOUT,
|
||||
}))
|
||||
.catch((err) => console.error('error logging out', err));
|
||||
};
|
||||
}
|
||||
@ -12,4 +12,4 @@ if (Notification.permission !== 'granted') {
|
||||
|
||||
render((
|
||||
<Layout />
|
||||
), null, document.getElementById('app'));
|
||||
), document.body);
|
||||
|
||||
67
client/components/Form.jsx
Normal file
67
client/components/Form.jsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
// modules > lodas
|
||||
import set from 'set-value';
|
||||
import { forEach, bindAll } from 'lowline';
|
||||
|
||||
// import Input from '../components/Input.jsx'
|
||||
|
||||
export default class Form extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
bindAll(this, ['register', 'validate']);
|
||||
|
||||
this.inputs = {};
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const attrs = {};
|
||||
|
||||
// eslint-disable-next-line
|
||||
for (let name in this.inputs) {
|
||||
const input = this.inputs[name];
|
||||
|
||||
const value = input.getValue();
|
||||
|
||||
if (value) set(attrs, name, value);
|
||||
}
|
||||
|
||||
return attrs;
|
||||
}
|
||||
|
||||
validate() {
|
||||
let errorCount = 0;
|
||||
|
||||
// eslint-disable-next-line
|
||||
for (let name in this.inputs) {
|
||||
const input = this.inputs[name];
|
||||
|
||||
if (!input.isValid()) {
|
||||
if (errorCount === 0) {
|
||||
input.focus();
|
||||
}
|
||||
|
||||
errorCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return !errorCount;
|
||||
}
|
||||
|
||||
register(name) {
|
||||
return (input) => {
|
||||
if (input) {
|
||||
input.validation = this.validation[input.props.name];
|
||||
|
||||
this.inputs[name] = input;
|
||||
} else {
|
||||
delete this.inputs[name];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
reset() {
|
||||
forEach(this.inputs, (input) => input.reset());
|
||||
}
|
||||
}
|
||||
98
client/components/FormElement.jsx
Normal file
98
client/components/FormElement.jsx
Normal file
@ -0,0 +1,98 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { bindAll, startCase } from 'lowline';
|
||||
|
||||
export default class Input extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
bindAll(this, ['reset', 'validate', 'onChange', 'onBlur', 'onFocus']);
|
||||
|
||||
this.state = {
|
||||
value: this.props.value,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
onChange(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (document.activeElement !== e.target && !e.target.value) return;
|
||||
|
||||
this.setValue(e.target.value);
|
||||
}
|
||||
|
||||
onBlur(e) {
|
||||
this.setState({
|
||||
focus: false,
|
||||
});
|
||||
|
||||
this.setValue(e.target.value);
|
||||
}
|
||||
|
||||
onFocus() {
|
||||
this.setState({
|
||||
focus: true,
|
||||
touched: true,
|
||||
});
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
isValid() {
|
||||
this.setValue(this.input.value);
|
||||
|
||||
return !this.state.error;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.state.value;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.setState({
|
||||
value: this.props.value,
|
||||
});
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
value = value || undefined;
|
||||
|
||||
const result = this.validate(value);
|
||||
|
||||
this.setState({
|
||||
error: result !== true ? typeof result === 'string' && result || 'Error!' : false,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
validate(value) {
|
||||
const validation = this.validation;
|
||||
|
||||
if (!validation) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (validation.required) {
|
||||
if (value == null || value === '') {
|
||||
return typeof validation.required === 'string' ? validation.required : 'Required';
|
||||
}
|
||||
} else if (value == null || value === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!validation.tests) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = 0; i < validation.tests.length; i += 1) {
|
||||
const [fnc, msg] = validation.tests[i];
|
||||
|
||||
if (!fnc(value)) return msg;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
38
client/components/Input.jsx
Normal file
38
client/components/Input.jsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { omit } from 'lowline';
|
||||
|
||||
import FormElement from './FormElement.jsx';
|
||||
|
||||
export default class Input extends FormElement {
|
||||
render({ type = 'text', disabled, placeholder }, state = {}) {
|
||||
const classes = Object.assign({
|
||||
'field-container': true,
|
||||
empty: !state.value,
|
||||
filled: state.value,
|
||||
focus: state.focus,
|
||||
invalid: state.error,
|
||||
touched: state.touched,
|
||||
valid: state.value && !state.error,
|
||||
});
|
||||
|
||||
return (
|
||||
<div class={classes}>
|
||||
<label className="placeholder">{placeholder}</label>
|
||||
<input
|
||||
disabled={disabled}
|
||||
onBlur={this.onBlur}
|
||||
onChange={this.onChange}
|
||||
onFocus={this.onFocus}
|
||||
onInput={this.onChange}
|
||||
placeholder={placeholder}
|
||||
ref={(input) => { this.input = input; }}
|
||||
type={type}
|
||||
value={state.value}
|
||||
/>
|
||||
<label class="icon" />
|
||||
{state.error && <label class="error">{state.error}</label>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,17 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
import Timer from './Timer.jsx';
|
||||
import LoginForm from './LoginForm.jsx';
|
||||
import User from './User.jsx';
|
||||
|
||||
export default () => (
|
||||
<div id="app">
|
||||
<main>
|
||||
<h1>Pomodoro Time!</h1>
|
||||
<User />
|
||||
<Timer type="pomodoro" time={25 * 60 * 1000} />
|
||||
<Timer type="break" time={5 * 60 * 1000} />
|
||||
<LoginForm />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
67
client/components/LoginForm.jsx
Normal file
67
client/components/LoginForm.jsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { h, Component } from 'preact';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
|
||||
import store from '../store';
|
||||
|
||||
import Form from './Form.jsx';
|
||||
import Input from './Input.jsx';
|
||||
|
||||
import { login } from '../actions/user';
|
||||
import { hideLoginModal } from '../actions/ui';
|
||||
|
||||
export default class LoginForm extends Form {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.validation = {
|
||||
email: {
|
||||
required: true,
|
||||
tests: [
|
||||
[isEmail, 'Not a valid email'],
|
||||
],
|
||||
},
|
||||
password: {
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
|
||||
this.state = {
|
||||
className: ['login-form', 'modal', 'enter', 'animate'],
|
||||
};
|
||||
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.unsubscribe = store.subscribe(() => this.forceUpdate());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
onSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.validate()) return;
|
||||
|
||||
store.dispatch(login(this.toJSON()));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { ui } = store.getState();
|
||||
|
||||
return ui.showLoginModal ? (
|
||||
<form
|
||||
className={this.state.className.join(' ')}
|
||||
onSubmit={this.onSubmit}
|
||||
>
|
||||
<button type="button" onClick={() => store.dispatch(hideLoginModal())}>Close</button>
|
||||
<h1>Login</h1>
|
||||
<Input ref={this.register('email')} type="text" name="email" placeholder="Email" />
|
||||
<Input ref={this.register('password')} type="password" name="password" placeholder="Password" />
|
||||
<button>Login</button>
|
||||
</form>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
@ -85,6 +85,7 @@ export default class Timer extends Component {
|
||||
fetch('/api/pomodoros', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
29
client/components/User.jsx
Normal file
29
client/components/User.jsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import store from '../store';
|
||||
|
||||
import { logout } from '../actions/user';
|
||||
import { showLoginModal } from '../actions/ui';
|
||||
|
||||
export default class User extends Component {
|
||||
componentWillMount() {
|
||||
this.unsubscribe = store.subscribe(() => this.forceUpdate());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { user } = store.getState();
|
||||
|
||||
return (user &&
|
||||
<div>
|
||||
<div>Logged in as {user.email}</div>
|
||||
<button onClick={() => store.dispatch(logout())}>Logout</button>
|
||||
</div> ||
|
||||
<div>
|
||||
<button onClick={() => store.dispatch(showLoginModal())}>Login</button>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,4 @@
|
||||
import Layout from './components/Layout.jsx';
|
||||
|
||||
export default ({ css, js }) => (
|
||||
export default ({ INITIAL_STATE, css, js }) => (
|
||||
'<!doctype html>' + (
|
||||
<html lang="en">
|
||||
<head>
|
||||
@ -16,8 +14,9 @@ export default ({ css, js }) => (
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Layout />
|
||||
|
||||
<script>
|
||||
var INITIAL_STATE = {INITIAL_STATE};
|
||||
</script>
|
||||
<script src={`/js/app${js ? js.suffix : ''}.js`} />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
9
client/reducers/root.js
Normal file
9
client/reducers/root.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import user from './user';
|
||||
import ui from './ui';
|
||||
|
||||
export default combineReducers({
|
||||
user,
|
||||
ui,
|
||||
});
|
||||
24
client/reducers/ui.js
Normal file
24
client/reducers/ui.js
Normal file
@ -0,0 +1,24 @@
|
||||
import {
|
||||
SHOW_LOGIN_MODAL,
|
||||
HIDE_LOGIN_MODAL,
|
||||
} from '../actions/ui';
|
||||
|
||||
const defaultState = {
|
||||
showLoginModal: false,
|
||||
};
|
||||
|
||||
export default (state = defaultState, action) => {
|
||||
switch (action.type) {
|
||||
case SHOW_LOGIN_MODAL:
|
||||
return Object.assign({}, state, {
|
||||
showLoginModal: true,
|
||||
});
|
||||
case HIDE_LOGIN_MODAL:
|
||||
return Object.assign({}, state, {
|
||||
showLoginModal: false,
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
13
client/reducers/user.js
Normal file
13
client/reducers/user.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { LOGIN_SUCCESS, LOGOUT } from '../actions/user';
|
||||
|
||||
export default (state = null, action) => {
|
||||
switch (action.type) {
|
||||
case LOGIN_SUCCESS:
|
||||
return action.payload;
|
||||
case LOGOUT:
|
||||
return null;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
16
client/store.js
Normal file
16
client/store.js
Normal file
@ -0,0 +1,16 @@
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import createLogger from 'redux-logger';
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
|
||||
const loggerMiddleware = createLogger();
|
||||
|
||||
import rootReducer from './reducers/root';
|
||||
|
||||
export default createStore(
|
||||
rootReducer,
|
||||
INITIAL_STATE,
|
||||
applyMiddleware(
|
||||
thunkMiddleware,
|
||||
loggerMiddleware
|
||||
)
|
||||
);
|
||||
@ -26,6 +26,7 @@
|
||||
"express": "^4.14.0",
|
||||
"express-service-errors": "github:thecodebureau/express-service-errors",
|
||||
"express-session": "^1.14.2",
|
||||
"get-value": "^2.0.6",
|
||||
"hyperscript-jsx": "0.0.2",
|
||||
"jsx-node": "^0.1.0",
|
||||
"lodash": "^4.16.6",
|
||||
@ -40,7 +41,13 @@
|
||||
"passport": "^0.3.2",
|
||||
"passport-local": "^1.0.0",
|
||||
"preact": "^6.4.0",
|
||||
"require-dir": "^0.3.1"
|
||||
"redux": "^3.6.0",
|
||||
"redux-logger": "^2.7.4",
|
||||
"redux-thunk": "^2.1.0",
|
||||
"require-dir": "^0.3.1",
|
||||
"set-value": "^0.4.0",
|
||||
"spineless": "^0.1.2",
|
||||
"validator": "^6.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^6.5.1",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const bootstrap = require('midwest/factories/bootstrap');
|
||||
const masterTemplate = require('../../build/master.jsx');
|
||||
|
||||
const router = new (require('express')).Router();
|
||||
@ -7,7 +8,9 @@ const router = new (require('express')).Router();
|
||||
router.get('/', (req, res, next) => {
|
||||
res.template = masterTemplate;
|
||||
|
||||
res.locals.user = req.user;
|
||||
|
||||
next();
|
||||
});
|
||||
}, bootstrap());
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user