nice design

This commit is contained in:
2018-06-01 19:45:10 +03:00
parent 2508a79d29
commit 6df56dc0b4
14 changed files with 293 additions and 159 deletions

View File

@@ -15,7 +15,8 @@
"allowTernary": true
}
],
"max-len": "off"
"max-len": "off",
"react/forbid-prop-types": "off"
},
"env": {
"browser": true

View File

@@ -8,6 +8,7 @@ export const LOGOUT = 'LOGOUT';
export const START_LOGIN = 'INVALIDATE_USER';
export const REQUEST_USER = 'REQUEST_USER';
export const VALIDATE_USER = 'VALIDATE_USER';
export const RESET_USER = 'RESET_USER';
function startLogin() {
return { type: START_LOGIN };
@@ -72,8 +73,8 @@ function signupSuccess(user) {
return { type: SIGNUP_SUCCESS, user };
}
function signupFail(errors) {
return { type: SIGNUP_FAIL, errors };
function signupFail(error) {
return { type: SIGNUP_FAIL, error };
}
export function signup(user) {
@@ -96,6 +97,10 @@ export function signup(user) {
};
}
export function reset() {
return { type: RESET_USER };
}
export function logout() {
localStorage.removeItem('jwt');
return { type: LOGOUT };

View File

@@ -1,13 +1,7 @@
body {
background: white;
color: black;
font-family: "Trebuchet MS";
user-select: none;
}
#lists-header {
display: flex;
justify-content: space-between;
background-color: #fbfbfb;
}
#lists {
@@ -43,34 +37,21 @@ body {
#listselector {
margin-left: 0.2rem;
align-self: center;
background-color: #fbfbfb;
}
#listselector select {
color: black;
font-size: 1.5rem;
font-weight: 500;
font-weight: 400;
outline: none;
border: none;
background: #fff;
}
#root {
margin-top: 5rem;
}
#container {
overflow: hidden;
margin: 0 auto;
padding: 0;
max-width: 25rem;
border: 1px solid #dddddd;
border-radius: 5px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
background-color: #fbfbfb;
}
#inputs {
transition: 0.4s ease-in-out;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1);
display: flex;
height: 2.5rem;
}
@@ -78,7 +59,7 @@ body {
#input {
color: black;
background: white;
font-family: "Trebuchet MS";
font-family: "Roboto";
box-sizing: border-box;
font-size: 1rem;
flex-grow: 1;
@@ -93,7 +74,7 @@ body {
#add {
color: black;
font-size: 1rem;
font-weight: 700;
font-weight: 500;
flex-grow: 1;
margin: 0;
padding: 0;
@@ -124,13 +105,13 @@ li button {
padding: 0.5rem;
text-align: center;
border: none;
border-top: 1px solid #dddddd;
border-top: 1px solid #f0f0f0;
background: none;
width: 2rem;
font-size: 1rem;
transition: 0.4s ease-in-out;
overflow: hidden;
box-shadow: inset -3px 0 6px -3px rgba(0, 0, 0, 0.5);
box-shadow: inset -3px 0 6px -3px rgba(0, 0, 0, 0.3);
}
li button.todo {
@@ -169,8 +150,9 @@ li {
word-wrap: break-word;
width: 100%;
padding: 0.5rem;
border-top: 1px solid #dddddd;
font-weight: 500;
padding-left: 1rem;
border-top: 1px solid #f0f0f0;
font-weight: 400;
flex-grow: 2;
flex-shrink: 1;
transition: 0.3s ease-in-out;
@@ -202,34 +184,12 @@ li {
color: #555555;
border: none;
background: none;
transition: 0.3s ease-in-out;
transition: 0.1s ease-in-out;
font-size: 1rem;
font-weight: 500;
font-weight: 300;
}
.filter--active {
font-weight: 700;
font-weight: 400;
color: black;
}
#user-header {
display: flex;
height: 2rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
justify-content: flex-end;
align-content: center;
}
.logout {
height: 100%;
flex-grow: 0;
flex-shrink: 1;
margin: 0.2rem;
padding: 0.2rem;
color: #555555;
border: none;
background: none;
transition: 0.3s ease-in-out;
font-size: 1rem;
font-weight: 500;
}

View File

@@ -3,7 +3,8 @@ import PropTypes from 'prop-types';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import 'normalize.css';
import '../App.css';
import './Container.css';
import './App.css';
import TodosContainer from '../containers/TodosContainer';
import LoginForm from '../components/user/LoginForm';

View File

@@ -0,0 +1,50 @@
@import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500");
body {
background: white;
color: black;
font-family: "Roboto";
user-select: none;
}
#root {
margin-top: 5rem;
}
#container {
overflow: hidden;
margin: 0 auto;
padding: 0;
max-width: 25rem;
border: 1px solid #dddddd;
border-radius: 7px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
#user-header {
display: flex;
height: 2rem;
position: relative;
z-index: 20;
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1);
justify-content: flex-end;
align-content: center;
}
#user-header button {
box-sizing: border-box;
margin-right: 0.2rem;
height: 100%;
flex-grow: 0;
flex-shrink: 1;
color: #888888;
border: none;
background: none;
transition: 0.1s ease-in-out;
font-size: 0.8rem;
font-weight: 300;
}
#user-header button:hover {
color: #555555;
}

22
src/components/Form.css Normal file
View File

@@ -0,0 +1,22 @@
#form {
padding: 2rem 1rem;
}
input {
border: none;
border-bottom: 1px solid #999999;
margin-left: 1rem;
height: 2rem;
}
.error {
margin: 1rem;
color: red;
}
form button {
padding: 0.25rem 1rem;
background: #f5f5f5;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1);
}

View File

@@ -6,17 +6,22 @@ import PropTypes from 'prop-types';
export default function ListActions({
addList, removeList, editList, list,
}) {
const editRemoveButtons = list
? [
<button onClick={() => removeList(list)}>
<FontAwesomeIcon icon={faTrash} />
</button>,
<button onClick={() => editList(list, prompt('List name?'))}>
<FontAwesomeIcon icon={faEdit} />
</button>,
]
: null;
return (
<div id="listactions">
<button onClick={() => addList(prompt('List name?'))}>
<FontAwesomeIcon icon={faPlus} />
</button>
<button onClick={() => removeList(list)}>
<FontAwesomeIcon icon={faTrash} />
</button>
<button onClick={() => editList(list, prompt('List name?'))}>
<FontAwesomeIcon icon={faEdit} />
</button>
{editRemoveButtons}
</div>
);
}

View File

@@ -75,7 +75,11 @@ class Todo extends React.Component {
);
const buttons = this.state.editing
? [
<animated.button key="save" className="save" onClick={() => this.stopEdit(input.value)}>
<animated.button
key="save"
className="save"
onClick={() => this.stopEdit(input.value)}
>
<FontAwesomeIcon icon={faCheck} />
</animated.button>,
]
@@ -87,7 +91,11 @@ class Todo extends React.Component {
>
<FontAwesomeIcon icon={faTrash} />
</animated.button>,
<animated.button key="edit" className={editClasses.join(' ')} onClick={this.startEdit}>
<animated.button
key="edit"
className={editClasses.join(' ')}
onClick={this.startEdit}
>
<FontAwesomeIcon icon={faEdit} />
</animated.button>,
];
@@ -115,7 +123,7 @@ Todo.propTypes = {
removeTodo: PropTypes.func.isRequired,
toggleTodo: PropTypes.func.isRequired,
editTodo: PropTypes.func.isRequired,
style: PropTypes.shape({ maxHeight: PropTypes.number.isRequired }).isRequired,
style: PropTypes.shape({ height: PropTypes.object.isRequired }).isRequired,
};
export default Todo;

View File

@@ -4,25 +4,30 @@ import { Transition } from 'react-spring';
import Todo from './Todo';
export default function TodosContainer(props) {
export default function TodosContainer({
todos,
toggleTodo,
removeTodo,
editTodo,
}) {
return (
<ul id="list">
<Transition
native
items={props.todos}
items={todos}
keys={todo => todo.id}
from={todo => ({ height: 0, borderColor: '#ffffff', opacity: 0.9 })}
enter={todo => ({ height: 60, borderColor: '#dddddd', opacity: 1 })}
leave={todo => ({ height: 0, borderColor: '#ffffff', opacity: 0.5 })}
from={{ height: 0, borderColor: '#f0f0f0', opacity: 0.9 }}
enter={{ height: 60, borderColor: '#f0f0f0', opacity: 1 }}
leave={{ height: 0, borderColor: '#ffffff', opacity: 0.5 }}
>
{props.todos.map(todo => styles => (
{todos.map(todo => styles => (
<Todo
key={todo.id}
todo={todo}
style={styles}
toggleTodo={() => props.toggleTodo(todo.id)}
removeTodo={() => props.removeTodo(todo.id)}
editTodo={text => props.editTodo(todo.id, text)}
toggleTodo={() => toggleTodo(todo.id)}
removeTodo={() => removeTodo(todo.id)}
editTodo={text => editTodo(todo.id, text)}
/>
))}
</Transition>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
import InputContainer from '../containers/InputContainer';
import TodoListContainer from '../containers/TodoListContainer';
@@ -22,3 +22,9 @@ export default function Todos({ user, loadLists, history }) {
</div>
);
}
Todos.propTypes = {
loadLists: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
};

View File

@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
export default function InputField({
required,
input,
label,
meta: { touched, error },
type,
}) {
return (
<div>
<label htmlFor={input.name}>
{label} <input required={required} {...input} type={type} />
</label>
{touched && error && <span className="error">{error}</span>}
</div>
);
}
InputField.propTypes = {
required: PropTypes.bool.isRequired,
input: PropTypes.any.isRequired,
label: PropTypes.string.isRequired,
meta: PropTypes.shape({
touched: PropTypes.bool,
error: PropTypes.string,
}).isRequired,
type: PropTypes.string.isRequired,
};

View File

@@ -2,62 +2,70 @@ import React from 'react';
import { Field, reduxForm } from 'redux-form';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import InputField from './InputField';
import { login } from '../../actions/user';
import '../Form.css';
function validate(values) {
const errors = {};
if (values.username === '') {
errors.username = 'should have username';
}
return errors;
}
const InputField = ({
input, label, meta: { touched, error }, type,
}) => (
<div>
<label htmlFor={input.name}>
{label} <input {...input} type={type} />
</label>
{touched && error && <span className="error">{error}</span>}
</div>
);
import { login, reset } from '../../actions/user';
function LoginForm({
handleSubmit, login, user, history,
handleSubmit, onLogin, user, history, resetUser,
}) {
let errors;
if (user.errors) {
if (user.errors.name === 'AuthenticationError') {
errors = <div>Wrong username or password</div>;
errors = <div className="error">Wrong username or password</div>;
}
}
if (user.user) {
history.push('/');
}
return (
<div id="login--form">
{errors}
<form onSubmit={handleSubmit(login)}>
<Field
label="username"
name="username"
component={InputField}
type="text"
/>
<Field
label="password"
name="password"
component={InputField}
type="password"
/>
<button type="submit">Submit</button>
</form>
<div>
<div id="user-header">
<button
onClick={() => {
resetUser();
history.push('/signup');
}}
>
signup
</button>
</div>
<div id="form">
{errors}
<form onSubmit={handleSubmit(onLogin)}>
<Field
label="username"
name="username"
required
component={InputField}
type="text"
/>
<Field
label="password"
name="password"
required
component={InputField}
type="password"
/>
<button type="submit">Submit</button>
</form>
</div>
</div>
);
}
LoginForm.propTypes = {
handleSubmit: PropTypes.func.isRequired,
onLogin: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
history: PropTypes.any.isRequired,
resetUser: PropTypes.func.isRequired,
};
function mapStateToProps(state) {
return {
user: state.user,
@@ -66,7 +74,9 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
login: ({ username, password }) => dispatch(login({ username, password })),
resetUser: () => dispatch(reset()),
onLogin: ({ username, password }) =>
dispatch(login({ username, password })),
};
}
@@ -76,5 +86,4 @@ export default reduxForm({
username: '',
password: '',
},
validate,
})(withRouter(connect(mapStateToProps, mapDispatchToProps)(LoginForm)));

View File

@@ -2,71 +2,92 @@ import React from 'react';
import { Field, reduxForm } from 'redux-form';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { signup } from '../../actions/user';
import InputField from './InputField';
import '../Form.css';
import { signup, reset } from '../../actions/user';
function validate(values) {
const errors = {};
if (values.username === '') {
errors.username = 'should have username';
}
if (values.password !== values.passwordRepeat) {
errors.passwordRepeat = 'Invalid';
}
return errors;
}
const InputField = ({
input, label, meta: { touched, error }, type,
}) => (
<div>
<label htmlFor={input.name}>
{label} <input {...input} type={type} />
</label>
{touched && error && <span className="error">{error}</span>}
</div>
);
function SignupForm({
handleSubmit, signup, user, history,
handleSubmit, onSignup, user, history, resetUser,
}) {
let errors;
if (user.errors) {
if (user.errors.name === 'AuthenticationError') {
errors = <div>Wrong username or password</div>;
errors = <div className="error">Wrong username or password</div>;
}
if (user.errors.name === 'ValidationError') {
if (user.errors.message.split(' ').includes('unique.')) {
errors = <div className="error">User already exists</div>;
} else {
errors = <div className="error">Validation error</div>;
}
}
}
if (user.user) {
history.push('/');
}
return (
<div id="signup--form">
{errors}
<form onSubmit={handleSubmit(signup)}>
<Field
label="username"
name="username"
component={InputField}
type="text"
/>
<Field
label="password"
name="password"
component={InputField}
type="password"
/>
<Field
label="repeat pasword"
name="passwordRepeat"
component={InputField}
type="password"
/>
<button type="submit">Submit</button>
</form>
<div>
<div id="user-header">
<button
onClick={() => {
resetUser();
history.push('/login');
}}
>
login
</button>
</div>
<div id="form">
{errors}
<form onSubmit={handleSubmit(onSignup)}>
<Field
label="username"
name="username"
required
component={InputField}
type="text"
/>
<Field
label="password"
name="password"
required
component={InputField}
type="password"
/>
<Field
label="repeat pasword"
name="passwordRepeat"
required
component={InputField}
type="password"
/>
<button type="submit">Submit</button>
</form>
</div>
</div>
);
}
SignupForm.propTypes = {
handleSubmit: PropTypes.func.isRequired,
onSignup: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
history: PropTypes.any.isRequired,
resetUser: PropTypes.func.isRequired,
};
function mapStateToProps(state) {
return {
user: state.user,
@@ -75,7 +96,8 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
signup: ({ username, password }) =>
resetUser: () => dispatch(reset()),
onSignup: ({ username, password }) =>
dispatch(signup({ username, password })),
};
}

View File

@@ -6,6 +6,7 @@ import {
SIGNUP_FAIL,
SIGNUP_SUCCESS,
VALIDATE_USER,
RESET_USER,
} from '../actions/user';
export default function user(
@@ -33,6 +34,7 @@ export default function user(
return {
...state,
user: action.user,
errors: null,
dirty: false,
fetching: false,
};
@@ -40,10 +42,18 @@ export default function user(
case LOGIN_FAIL:
return {
...state,
user: null,
errors: action.error,
dirty: false,
fetching: false,
};
case RESET_USER:
return {
...state,
fetching: false,
user: null,
errors: null,
};
case LOGOUT:
return {
...state,