diff --git a/api/cart.js b/api/cart.js
index c9e6cd4..0062c4a 100644
--- a/api/cart.js
+++ b/api/cart.js
@@ -29,3 +29,11 @@ router.post('/add/:item_uuid', parseJSON, cartAddValidators, async (req, res) =>
res.json(cart)
})
+
+router.post('/remove/:item_uuid', async (req, res) => {
+ if(!req.cart) return res.json(null)
+
+ const cart = await db.cart.removeItemFromCart(req.cart.uuid, req.params.item_uuid);
+
+ res.json(cart)
+})
\ No newline at end of file
diff --git a/api/middleware/session.js b/api/middleware/session.js
index ee86ba4..71b9f41 100644
--- a/api/middleware/session.js
+++ b/api/middleware/session.js
@@ -4,7 +4,8 @@ const sessionMiddleware = (req, res, next)=>{
(async ()=>{
let session = await db.session.validate(req.session.uuid);
- if(!session) return;
+ if(!session)
+ return req.session.uuid = null;
// Update last active
session = await db.session.update(req.session.uuid);
diff --git a/components/form/form.js b/components/form/form.js
index 00949ac..c7b95ff 100644
--- a/components/form/form.js
+++ b/components/form/form.js
@@ -85,7 +85,9 @@ export const FormController = function FormController({children, className, url
name: child.props.name,
validate: child.props.validate,
value: child.props.initialValue || "",
- isValid: child.props.initialValue ? child.props.validate(child.props.initialValue): false,
+ isValid: child.props.validate
+ ?(child.props.initialValue ? child.props.validate(child.props.initialValue): false)
+ :true,
touched: false
}
})
@@ -106,7 +108,8 @@ export const FormController = function FormController({children, className, url
if(url)
try {
- await axios({ method, url, data })
+ const {data: response} = await axios({ method, url, data })
+ return afterSubmit(response)
} catch (err) {
if(!err.response || err.response.status !== 422) throw err;
@@ -123,7 +126,8 @@ export const FormController = function FormController({children, className, url
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),
+ // Allow enabled prop to disable, but not solely enable
+ enabled: child.props.enabled !== false && !Object.values(state.fields).some(field=>!field.isValid),
onClick: handleSubmit
})
diff --git a/components/form/input.js b/components/form/input.js
index 20cd2a2..1ef9b05 100644
--- a/components/form/input.js
+++ b/components/form/input.js
@@ -7,7 +7,7 @@ export default function Input({label, error, hint, type, name, value, onChange,
- {error || (isValid ? '' : hint)}
+ {hint && {error || (isValid ? '' : hint)}}
)
}
diff --git a/components/form/numInput.js b/components/form/numInput.js
index cad09cb..96ab610 100644
--- a/components/form/numInput.js
+++ b/components/form/numInput.js
@@ -18,7 +18,7 @@ export default function Input({label, minimum = 0, maximum, error, hint, name, v
- {error || (isValid ? '' : hint)}
+ {hint && {error || (isValid ? '' : hint)}}
)
}
diff --git a/components/table/table.js b/components/table/table.js
new file mode 100644
index 0000000..8dee501
--- /dev/null
+++ b/components/table/table.js
@@ -0,0 +1,33 @@
+import styles from './table.module.css'
+
+export default function Table({columns, rows, foot}) {
+ return (
+
+
+
+ {columns.map(column=>
+ {column.name} |
+ )}
+
+
+
+ {rows.map(row=>
+
+ {columns.map(column=>
+ {column.extractor(row)} |
+ )}
+
+ )}
+
+ {foot &&
+
+ {foot.map((item, i) =>
+
+ {item}
+ |
+ )}
+
+ }
+
+ )
+}
\ No newline at end of file
diff --git a/components/table/table.module.css b/components/table/table.module.css
new file mode 100644
index 0000000..ae4c23a
--- /dev/null
+++ b/components/table/table.module.css
@@ -0,0 +1,24 @@
+.table {
+ width: calc(100% - 100px);
+ max-width: 800px;
+ margin: 0 auto;
+ background: white;
+ border-collapse: collapse;
+ border: solid 1px gray;
+}
+
+.table td, .table th {
+ border-top: solid 1px gray;
+ border-bottom: solid 1px gray;
+ text-align: left;
+}
+
+.table th {
+ padding: 5px 15px;
+ font-family: 'Cormorant SC',serif;
+ font-weight: bold;
+}
+
+.table td {
+ padding: 15px;
+}
\ No newline at end of file
diff --git a/db/models/cart.js b/db/models/cart.js
index 39856f6..f5dd6e3 100644
--- a/db/models/cart.js
+++ b/db/models/cart.js
@@ -35,13 +35,12 @@ cart.addItemToCart = async (cart_uuid, item_uuid, amount) => {
return joinjs.map(rows, mappings, 'cartMap', 'cart_')[0];
}
-cart.removeItemFromCart = async (cart_uuid, item_uuid, amount) => {
+cart.removeItemFromCart = async (cart_uuid, item_uuid) => {
const query = {
- text: 'select * from sos.remove_item_from_cart($1, $2, $3)',
+ text: 'select * from sos.remove_item_from_cart($1, $2)',
values: [
cart_uuid,
- item_uuid,
- amount
+ item_uuid
]
}
diff --git a/db/sql/3-functions.sql b/db/sql/3-functions.sql
index 852fa57..dc76bc1 100644
--- a/db/sql/3-functions.sql
+++ b/db/sql/3-functions.sql
@@ -284,3 +284,33 @@ begin
-- Return cart
return query select * from sos.v_cart where cart_uuid = _cart_uuid;
end; $function$;
+
+create or replace function sos.remove_item_from_cart(_cart_uuid uuid, _item_uuid uuid)
+ returns setof sos.v_cart
+ language plpgsql
+as $function$
+declare
+ _row_count integer;
+begin
+ -- Check if we already have a row for this combination
+ select
+ count(*)
+ into _row_count
+ from sos.cart_item
+ where cart_item_cart_uuid = _cart_uuid
+ and cart_item_item_uuid = _item_uuid
+ group by cart_item_uuid, cart_item_count;
+
+ -- If we do not have a row, just exit
+ if _row_count < 1 then
+ return query select * from sos.v_cart where cart_uuid = _cart_uuid;
+ end if;
+
+ -- If we have a row, then delete it
+ delete from sos.cart_item
+ where cart_item_cart_uuid = _cart_uuid
+ and cart_item_item_uuid = _item_uuid;
+
+ -- Return cart
+ return query select * from sos.v_cart where cart_uuid = _cart_uuid;
+end; $function$;
diff --git a/pages/_app.js b/pages/_app.js
index f4cd337..b1ade88 100644
--- a/pages/_app.js
+++ b/pages/_app.js
@@ -1,4 +1,4 @@
-import React from "react"
+import React, {useState} from "react"
import PropTypes from "prop-types"
import axios from 'axios'
@@ -29,11 +29,13 @@ Layout.getInitialProps = async ({Component, ctx}) => {
return {pageProps, user, cart}
}
-function Layout({ Component, pageProps, user, cart }){
+function Layout({ Component, pageProps, user, cart: _cart }){
+ const [cart, setCart] = useState(_cart)
+
return (
<>
-
+
>
)
diff --git a/pages/store/cart.js b/pages/store/cart.js
index 1919818..a81775e 100644
--- a/pages/store/cart.js
+++ b/pages/store/cart.js
@@ -1,18 +1,51 @@
-import React, {useState} from 'react'
+import React from 'react'
+import axios from 'axios'
import Head from 'next/head'
-// import styles from './cart.module.css'
+import {FormController, Input, Button} from '~/components/form'
+import Table from '~/components/table'
+
+export default function Cart({cart, setCart}){
+ const handleRemove = id => async ev => {
+ if(ev) ev.preventDefault()
+
+ const {data} = await axios.post(`/api/cart/remove/${id}`)
+ setCart(data)
+ }
-// TODO: Cart page
-export default function Cart({cart}){
return (
<>
Cart
-
-
- {JSON.stringify(cart, null, 2)}
-
-
+ <>
+ Cart
+ row.item.name},
+ {name: 'Quantity in Cart', extractor: row => row.count},
+ {name: 'Price Each', extractor: row => '$' + (row.item.price_cents / 100).toFixed(2)},
+ {name: 'Total Price', extractor: row => '$' + (row.count * row.item.price_cents / 100).toFixed(2)},
+ {name: '', extractor: row =>
+
+ }
+ ]}
+ rows={cart?.items?.map(row=>({
+ ...row,
+ id: row.item.uuid
+ }))}
+ foot={[
+ 'Total:',
+ cart?.items.map(r=>r.count).reduce((a,b) => (a+b), 0) || 0,
+ '',
+ '$' + ((cart?.items.map(r=>r.count * r.item.price_cents).reduce((a,b) => (a+b), 0) || 0) / 100).toFixed(2),
+ ''
+ ]}
+ />
+
+
+
+
+
+ >
>
)
}
diff --git a/pages/store/item/[slug].js b/pages/store/item/[slug].js
index b94484e..84001d0 100644
--- a/pages/store/item/[slug].js
+++ b/pages/store/item/[slug].js
@@ -19,12 +19,12 @@ Item.getInitialProps = async function({ctx: {axios, query: {slug}}}){
}
// TODO: Modal with full image size on clicking preview
-export default function Item({item, cart}){
+export default function Item({item, cart, setCart}){
// Pick first one with featured flag or 0
const featuredIndex = item.images.reduce((p, im, i) => ((p !== undefined) ? p : (im.featured ? i : undefined)), undefined) || 0
const [selectedIndex, setSelected] = useState(featuredIndex);
- const numInCart = cart?.items.find(i => i.item.uuid === item.uuid)?.count || 0
+ const numInCart = cart?.items?.find(i => i.item.uuid === item.uuid)?.count || 0
return (
<>
@@ -53,8 +53,8 @@ export default function Item({item, cart}){
{item.name}
{ item.description.split('\n').map(p=>p.trim()).filter(p=>p !== '').map((p,i)=>{p}
) }
- Router.push('/store/cart')}>
- isNum(value, {no_symbols: true})} hint="Enter the number of socks to add to your cart" />
+ {setCart(cart); Router.push('/store/cart')}}>
+ isNum(value, {no_symbols: true})} />
diff --git a/styles/layout.css b/styles/layout.css
index 7a51afd..20fb2af 100644
--- a/styles/layout.css
+++ b/styles/layout.css
@@ -29,6 +29,11 @@ main {
main h2 {
font-family: 'Cormorant SC',serif;
font-size: 36px;
+ font-weight: normal;
+}
+
+main > h2:first-child {
+ text-align: center;
}
main {
@@ -46,6 +51,21 @@ main {
column-count: 3;
}
+button.buttonLink {
+ background: none;
+ border: none;
+ color: blue;
+ text-decoration: underline;
+ font-family: 'Cormorant Infant', serif;
+ font-size: 16px;
+ cursor: pointer;
+ outline: none;
+}
+
+button.buttonLink:focus {
+ color: rgb(0, 0, 175);
+}
+
@media (max-width:1200px){
.cardContainer{
padding:0 2%;