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 (
- <>
+
)
}
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 (
-
+ {label}:
+
+ {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 = () => (
-
-)
-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" />
+ Submit
+
+ )
+}