diff --git a/api/auth.js b/api/auth.js
index df1fdcc..638669c 100644
--- a/api/auth.js
+++ b/api/auth.js
@@ -37,7 +37,9 @@ router.post('/', parseJSON, loginValidation, async (req, res) => {
req.session.uuid = session.uuid
- res.json(user)
+ const {password_hash, ...result} = user
+
+ res.json(result)
})
// TODO: Login link stuff
diff --git a/api/cart.js b/api/cart.js
new file mode 100644
index 0000000..96910dd
--- /dev/null
+++ b/api/cart.js
@@ -0,0 +1,13 @@
+const router = module.exports = require('express-promise-router')()
+const parseJSON = require('body-parser').json()
+const db = require('../db')
+
+// TODO: Actually implement cart!
+
+router.get('/', async (req, res)=>{
+ res.json(null);
+})
+
+router.post('/add', parseJSON, async (req, res) => {
+ res.json(null)
+})
\ No newline at end of file
diff --git a/api/index.js b/api/index.js
index 422b1e9..a407ffe 100644
--- a/api/index.js
+++ b/api/index.js
@@ -17,6 +17,7 @@ router.use((req, res, next)=>{
router.use(require('cookie-session')({name: 'sos-session', secret: process.env.COOKIE_SECRET}))
router.use(require('./middleware/session'))
router.use('/auth', require('./auth'))
+router.use('/cart', require('./cart'))
router.use('/users/', require('./users'))
router.use('/items/', require('./items'))
router.use('/images/', require('./images'))
diff --git a/api/items.js b/api/items.js
index 90c2b4a..86272be 100644
--- a/api/items.js
+++ b/api/items.js
@@ -44,6 +44,11 @@ router.post('/', parseJSON, newItemValidators, async (req, res) => {
res.json(item)
})
+router.get('/by-slug/:slug', async (req, res) => {
+ const item = await db.item.findBySlug(req.params.slug);
+ res.json(item)
+})
+
router.delete('/:uuid', async (req, res) => {
const result = await db.item.removeItem(req.params.uuid)
res.json({deleted: true})
diff --git a/components/card/card.js b/components/card/card.js
index bf5ba3f..6a6bfc9 100644
--- a/components/card/card.js
+++ b/components/card/card.js
@@ -9,7 +9,7 @@ export default function Card({item, numberInCart}) {
return (
-
+
{featuredImage &&
}
diff --git a/components/form/form.js b/components/form/form.js
index 65bfd36..00949ac 100644
--- a/components/form/form.js
+++ b/components/form/form.js
@@ -4,6 +4,7 @@ import axios from 'axios'
import styles from './styles.module.css'
export const Input = require('./input.js').default
+export const NumInput = require('./numInput.js').default
export const Button = require('./button.js').default
const errorReducer = (errors, action) => {
@@ -15,7 +16,6 @@ const errorReducer = (errors, action) => {
return newErrors
case 'clear_error':
- console.log('clear_error', action)
return {
...errors,
[action.field]: undefined
@@ -72,7 +72,7 @@ const formReducer = errorDispatch => (state, action)=>{
}
-export const FormController = function FormController({children, url, method = 'POST', afterSubmit = ()=>null}){
+export const FormController = function FormController({children, className, url, method = 'POST', afterSubmit = ()=>null}){
const initialState = {
fields: {}
}
@@ -85,7 +85,7 @@ export const FormController = function FormController({children, url, method =
name: child.props.name,
validate: child.props.validate,
value: child.props.initialValue || "",
- isValid: false,
+ isValid: child.props.initialValue ? child.props.validate(child.props.initialValue): false,
touched: false
}
})
@@ -139,7 +139,7 @@ export const FormController = function FormController({children, url, method =
})
return (
-
)
diff --git a/components/form/numInput.js b/components/form/numInput.js
new file mode 100644
index 0000000..7a39841
--- /dev/null
+++ b/components/form/numInput.js
@@ -0,0 +1,24 @@
+import React from 'react'
+
+import styles from './styles.module.css'
+
+export default function Input({label, minimum = 0, maximum, error, hint, name, value, onChange, onBlur, isValid}){
+ const current = parseInt(value, 10) || 0
+ const nextUp = maximum !== undefined ? Math.min(current + 1, maximum) : current + 1
+ const nextDown = minimum !== undefined ? Math.max(current - 1, minimum) : current - 1
+
+ const valueUp = ()=>onChange({target: {value: nextUp + ""}})
+ const valueDown = ()=>onChange({target: {value: nextDown + ""}})
+
+ return (
+
+
{label}:
+
+ -
+
+ +
+
+
{error || (isValid ? '' : hint)}
+
+ )
+}
diff --git a/components/form/styles.module.css b/components/form/styles.module.css
index dcc56f8..5c9479f 100644
--- a/components/form/styles.module.css
+++ b/components/form/styles.module.css
@@ -19,37 +19,60 @@
}
.formElementContainer input {
+ padding: 10px;
+}
+
+.formElementContainer input, .formElementContainer .complexInput {
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;
}
+:global(.dark) .formElementContainer input, :global(.dark) .formElementContainer .complexInput {
+ border: none;
+}
+
+.complexInput {
+ display: flex;
+ flex-direction: row;
+ overflow: hidden;
+}
+
+.complexInput > button {
+ width: 38px;
+ display: inline-block;
+}
+
+.complexInput > input {
+ flex: 1;
+ border: 0;
+ border-radius: 0;
+}
+
.formElementContainer span.hint {
display: block;
margin-top: 8px;
- color: red;
+ color: var(--error-color);
opacity: 0;
transition: .2s opacity;
user-select: none;
height: 18.5px;
}
-.formElementContainer input.invalid {
- border: solid 1px red;
- box-shadow: red;
+.formElementContainer .invalid {
+ border: solid 1px var(--error-color);
+ box-shadow: var(--error-color);
}
-.formElementContainer input.invalid + span.hint {
- opacity: .5;
+.formElementContainer .invalid + span.hint {
+ opacity: 1;
}
.formElementContainer button {
- width: 100%;
min-height: 20px;
background: rgb(156, 39, 176);
color: white;
@@ -60,6 +83,10 @@
transition-duration: .2s;
}
+.formElementContainer > button {
+ width: 100%;
+}
+
.formElementContainer button[type="submit"] {
margin-top: 10px;
}
diff --git a/db/models/item.js b/db/models/item.js
index 76369ee..9e03bac 100644
--- a/db/models/item.js
+++ b/db/models/item.js
@@ -46,7 +46,7 @@ item.findBySlug = async (item_slug) => {
item.create = async (name, urlslug, description, price_cents, published) => {
const query = {
- text: 'select * from sos..create_item($1::text, $2::citext, $3::text, $4::integer, $5::boolean)',
+ text: 'select * from sos.create_item($1::text, $2::citext, $3::text, $4::integer, $5::boolean)',
values: [
name,
urlslug,
diff --git a/pages/_app.js b/pages/_app.js
index 48e4495..6657417 100644
--- a/pages/_app.js
+++ b/pages/_app.js
@@ -9,23 +9,31 @@ import "../styles/layout.css"
Layout.getInitialProps = async ({Component, ctx}) => {
// Configure axios instance
ctx.axios = axios.create({
- headers: ctx.req ? {cookie: ctx.req.headers.cookie} : undefined
+ headers: (ctx.req && ctx.req.headers && ctx.req.headers.cookie)
+ ? {cookie: ctx.req.headers.cookie}
+ : undefined
})
- const {data: user} = await ctx.axios.get(`/api/auth`)
+ const promises = {
+ user: ctx.axios.get(`/api/auth`),
+ cart: ctx.axios.get(`/api/cart`)
+ }
+
+ const {data: user} = await promises.user
+ const {data: cart} = await promises.cart
let pageProps = {};
if(Component.getInitialProps)
pageProps = await Component.getInitialProps({ctx})
- return {pageProps, user}
+ return {pageProps, user, cart}
}
-function Layout({ Component, pageProps, user }){
+function Layout({ Component, pageProps, user, cart }){
return (
<>
-
+
>
)
diff --git a/pages/store/item/[slug].js b/pages/store/item/[slug].js
index 03c5575..25987ff 100644
--- a/pages/store/item/[slug].js
+++ b/pages/store/item/[slug].js
@@ -1,17 +1,82 @@
-import React from 'react'
-import {useRouter} from 'next/router'
+import React, {useState} from 'react'
+import Head from 'next/head'
+import Router from 'next/router'
+import {FormController, NumInput, Button} from '~/components/form'
+import isNum from 'validator/lib/isNumeric'
-Item.getInitialProps = async function({query}){
- return {}
+import styles from './slug.module.css'
+
+Item.getInitialProps = async function({ctx: {axios, query: {slug}}}){
+ const {data: item} = await axios.get(`/api/items/by-slug/${slug}`)
+
+ if(!item) {
+ const err = new Error("Not found")
+ err.status = 404
+ throw err;
+ }
+
+ return {item}
}
-export default function Item(){
- const router = useRouter()
- const {slug} = router.query
+// TODO: Modal with full image size on clicking preview
+export default function Item({item, cart}){
+ // 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 ? cart.items.filter(i => i.uuid === item.uuid).length : 0
return (
-
+ <>
+
{item.name}
+
+
+ {item.images && item.images.length > 0 && (
+
+
+
+
+ {item.images && item.images.length > 1 &&
+
+ {item.images.map((image, index) => (
+
setSelected(index)}
+ className={index === selectedIndex ? styles.selectedImage : undefined}
+ src={`/api/images/${image.uuid}/thumb`}
+ />
+ ))}
+
+ }
+
+ )}
+
+
{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" />
+ Add to cart
+
+
+
+ Price: ${(parseInt(item.price_cents) / 100).toFixed(2)} each
+
+ {
+ item.number_in_stock
+ ? item.number_in_stock + ' in stock'
+ : 'Out of stock'
+ }
+
+ {numInCart} in cart
+
+
+
+
+
Additional Information
+
All socks are only available in adult medium sizes (approximately US Men's size 10 - 13 depending on stretching) at the moment - we're working on getting more sizes available but not quite ready for that yet.
+
All purchases are shipped within a few days of your purchase, (USPS first class).
+
+
+ >
)
}
diff --git a/pages/store/item/slug.module.css b/pages/store/item/slug.module.css
new file mode 100644
index 0000000..46cc17d
--- /dev/null
+++ b/pages/store/item/slug.module.css
@@ -0,0 +1,96 @@
+.pageContainer {
+ display: flex;
+ flex-direction: column;
+ --selection-border: 3px;
+}
+
+.itemDetails {
+ display: flex;
+ flex-direction: row;
+ min-height: 400px;
+ background: rgb(33, 32, 34);
+ color: white;
+ justify-content: center;
+ align-items: center;
+}
+
+.card {
+ max-height: 250px;
+ min-height: 0px;
+ background: rgb(236, 172, 255);
+ box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
+ padding: 5px;
+}
+
+.card img {
+ max-height: 250px;
+ max-width: 200px;
+ object-fit: cover;
+}
+
+
+.imageSelector img {
+ margin-top: 15px;
+ margin-left: 15px;
+ display: inline-block;
+ height: 50px;
+ object-fit: cover;
+ overflow: hidden;
+ position: relative;
+}
+
+.imageSelector img:first-child:not(.selectedImage) {
+ margin-left: var(--selection-border);
+}
+
+.imageSelector img.selectedImage {
+ border: solid var(--selection-border) rgb(223, 118, 255);
+ margin-top: calc(15px - var(--selection-border));
+ margin-left: calc(15px - var(--selection-border));
+}
+
+.imageSelector img.selectedImage + img {
+ margin-left: calc(15px - var(--selection-border));
+}
+
+.imageSelector img.selectedImage:first-child {
+ margin-left: 0;
+}
+
+.imageSelector img:not(.selectedImage) {
+ margin-bottom: var(--selection-border);
+ cursor: pointer;
+}
+
+.controls {
+ flex: 1;
+ max-width: 500px;
+ margin-left: 40px;
+}
+
+.controls > h2:first-child {
+ margin-top: 0;
+ margin-bottom: 10px;
+}
+
+.notes {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+.moreDetails {
+ flex: 1;
+ max-width: 800px;
+ margin: 0 auto;
+ width: calc(100vw - 100px);
+}
+
+.moreDetails h2 {
+ text-align: center;
+}
+
+.form > div {
+ margin-left: 0;
+ width: 100%;
+}
\ No newline at end of file
diff --git a/styles/layout.css b/styles/layout.css
index 1de8307..7a51afd 100644
--- a/styles/layout.css
+++ b/styles/layout.css
@@ -13,12 +13,26 @@ html,body {
margin: 0;
padding: 0;
background: #313131;
+ --error-color: rgba(255, 0, 0, 0.5);
+}
+
+.dark {
+ --error-color: rgb(255, 75, 75);
}
main {
background: #E4E4E4;
color: black;
- padding: 30px 0;
+ padding: 15px 0;
+}
+
+main h2 {
+ font-family: 'Cormorant SC',serif;
+ font-size: 36px;
+}
+
+main {
+ font-family: 'Cormorant Infant',serif;
}
.cardContainer{