diff --git a/api/index.js b/api/index.js index 602afc0..b294fe6 100644 --- a/api/index.js +++ b/api/index.js @@ -1,6 +1,8 @@ -const router = require('express-promise-router')() +const {Router} = require('express') const pg = require('../db/pg') +const router = module.exports = new Router() + router.use((req, res, next)=>{ // Skip pretty-printing if in prod if(req.app.locals.dev) @@ -12,6 +14,7 @@ router.use((req, res, next)=>{ next() }) +router.use('/auth', require('./auth')) router.use('/users/', require('./users')) router.use('/items/', require('./items')) router.use('/images/', require('./images')) @@ -21,11 +24,17 @@ router.get('/', (req, res)=>{ res.json({test: true}) }) -router.use((req, res)=>{ - res.status(404) +router.use((req, res, next)=>{ + const err = new Error('Not found') + err.status = 404 + return next(err); +}) + +router.use((err, req, res, next)=> { + console.error(err.stack) + + res.status(err.status || 500) res.json({ - error: 'Not found' + error: err.message }) }) - -module.exports = router; diff --git a/components/form/button.js b/components/form/button.js new file mode 100644 index 0000000..c0a160d --- /dev/null +++ b/components/form/button.js @@ -0,0 +1,11 @@ +import React from 'react' + +import styles from './styles.module.css' + +export default function Button({onClick, enabled, type, children}){ + return ( +
+
+ ) +} diff --git a/components/form/form.js b/components/form/form.js index de07d52..b9235c3 100644 --- a/components/form/form.js +++ b/components/form/form.js @@ -1,7 +1,9 @@ // TODO: Enable exportDefaultFrom in Babel syntax import React, { useReducer } from 'react' +import styles from './styles.module.css' export const Input = require('./input.js').default +export const Button = require('./button.js').default export const FormController = function FormController({children, onSubmit}){ const initialState = { @@ -10,12 +12,25 @@ export const FormController = function FormController({children, onSubmit}){ const reducer = (state, action)=>{ let fields = {...state.fields} + const prevField = state.fields[action.name] + + // If no value, just mark touched + if(action.value === undefined) + return { + fields: { + ...fields, + [action.name]: { + ...prevField, + touched: true, + isValid: prevField.validate?prevField.validate(prevField.value, fields):true + } + } + } // Update currently changing field - const prevField = state.fields[action.name] const updatedField = { ...prevField, - value: action.value, + value: action.value !== undefined ? action.value : prevField.value, isValid: prevField.validate?prevField.validate(action.value, fields):true } fields[action.name] = updatedField @@ -35,7 +50,7 @@ export const FormController = function FormController({children, onSubmit}){ } return {fields} -} + } // Update initial state React.Children.forEach(children, child => { @@ -44,32 +59,47 @@ export const FormController = function FormController({children, onSubmit}){ initialState.fields[child.props.name] = { name: child.props.name, validate: child.props.validate, - value: child.props.initialValue, - isValid: true + value: child.props.initialValue || "", + isValid: false, + touched: false } }) // Create reducer const [state, dispatch] = useReducer(reducer, initialState) + const submitForm = ev=>{ + if(ev) ev.preventDefault(); + + const fields = {} + for(const name in state.fields){ + const field = state.fields[name] + fields[field.name] = field.value + } + + onSubmit(fields) + } + const _children = React.Children.map(children, child => { + if(child.type === Button && child.props.type.toLowerCase() === 'submit') + return React.cloneElement(child, { + enabled: !Object.values(state.fields).some(field=>!field.isValid), + onClick: submitForm + }) + const {name} = child.props; if(!name) return child; - - const newProps = { + return React.cloneElement(child, { onChange: ev=>dispatch({name, value: ev.target.value}), + onBlur: ev=>dispatch({name}), value: state.fields[name].value, - isValid: "" + state.fields[name].isValid - } - - return React.cloneElement(child, newProps) + isValid: state.fields[name].touched ? state.fields[name].isValid : true + }) }) - console.log(JSON.stringify(state.fields, null, 2)) - return ( - <> +
{_children} - +
) } diff --git a/components/form/input.js b/components/form/input.js index c097849..e1abd71 100644 --- a/components/form/input.js +++ b/components/form/input.js @@ -2,10 +2,12 @@ import React from 'react' import styles from './styles.module.css' -export default function Input({type, name, value, onChange, isValid}){ +export default function Input({label, hint, type, name, value, onChange, onBlur, isValid}){ return (
- + + + {hint}
) } diff --git a/components/form/styles.module.css b/components/form/styles.module.css index ecf0bbd..1957400 100644 --- a/components/form/styles.module.css +++ b/components/form/styles.module.css @@ -1,3 +1,69 @@ -.formElementContainer { +.formContainer > *:first-child { + margin-top: 20px; +} + +.formContainer > * { + display: block; + align-items: center; + width: 400px; + max-width: 80vw; + margin: 10px auto; + margin-bottom: 20px; +} + +.formElementContainer label { + width: 150px; + display: block; + font-weight: bold; + margin-bottom: 8px; +} + +.formElementContainer input { + box-sizing: border-box; + width: 100%; + min-width: 0; + padding: 10px; + border: solid 1px rgba(0,0,0,.2); + border-radius: 2px; + transition-property: border-color,box-shadow; + transition: .2s ease-in-out; +} + +.formElementContainer span.hint { display: block; + margin-top: 8px; + color: red; + opacity: 0; + transition: .2s opacity; + user-select: none; +} + +.formElementContainer input.invalid { + border: solid 1px red; + box-shadow: red; +} + +.formElementContainer input.invalid + span.hint { + opacity: .5; +} + +.formElementContainer button { + width: 100%; + min-height: 20px; + background: rgb(156, 39, 176); + color: white; + padding: 10px 0; + border: none; + box-shadow: none; + transition-property: opacity filter; + transition-duration: .2s; +} + +.formElementContainer button[type="submit"] { + margin-top: 10px; } + +.formElementContainer button[disabled] { + opacity: .4; + filter: saturate(.2); +} \ No newline at end of file diff --git a/db/index.js b/db/index.js index 9e8ea1a..0add5a9 100644 --- a/db/index.js +++ b/db/index.js @@ -1,5 +1,6 @@ module.exports = { user: require('./models/user'), item: require('./models/item'), - category: require('./models/category') + category: require('./models/category'), + user: require('./models/user') } \ No newline at end of file diff --git a/db/models/user.js b/db/models/user.js index 47b15f2..7243930 100644 --- a/db/models/user.js +++ b/db/models/user.js @@ -55,18 +55,18 @@ user.register = async (email, password) => { } user.login = async (email, password) => { - const user = await user.findByEmail(email) + const _user = await user.findByEmail(email) - if(!user){ + if(!_user){ // Avoid early exit timing difference - await bcrypt.hash(password) + await bcrypt.hash(password, saltRounds) throw new Error("User not found") } - const passwordCorrect = await bcrypt.compare(password, user.password_hash) + const passwordCorrect = await bcrypt.compare(password, _user.password_hash) if(!passwordCorrect) throw new Error("Password incorrect") - return user + return _user } diff --git a/package.json b/package.json index 2f2aaa4..6a0ca51 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "pg": "^7.18.1", "react": "^16.12.0", "react-dom": "^16.12.0", - "sharp": "^0.24.1" + "sharp": "^0.24.1", + "validator": "^12.2.0" } } diff --git a/pages/form.js b/pages/form.js deleted file mode 100644 index 6a17d3a..0000000 --- a/pages/form.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react' - -import {FormController, Input} from '~/components/form' - -const Index = () => ( - <> -

Form Controller Test

- - (value.length >= 8)} initialValue="" /> - - -) -export default Index - -// Help Almyki figure out how to do 4 spaces python, 2 spaces html -// also show her how to add a vertical bar diff --git a/pages/login.js b/pages/login.js index 3ecd8aa..1305320 100644 --- a/pages/login.js +++ b/pages/login.js @@ -1,8 +1,21 @@ import React from 'react' +import isEmail from 'validator/lib/isEmail' +import axios from 'axios' -const Login = () => ( -
-

Login

-
-) -export default Login +import {FormController, Input, Button} from '~/components/form' + +export default function Login(){ + const submit = async (values)=>{ + const {data} = await axios.post(`/api/auth`, values) + console.log(data) + } + + return ( + +

Login

+ isEmail(value)} hint="Enter a valid email address" /> + (value.length >= 8)} hint="Password must be at least 8 characters long" /> + +
+ ) +}