Item page - rudimentary cart hookups

main
Ashelyn Dawn 5 years ago
parent 8862e5dac0
commit eeee3c3d49

@ -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

@ -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)
})

@ -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'))

@ -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})

@ -9,7 +9,7 @@ export default function Card({item, numberInCart}) {
return (
<div className={styles.card}>
<h3><Link href={`/store/sock/${item.urlslug}`}><a>{item.name}</a></Link></h3>
<h3><Link href={`/store/item/${item.urlslug}`}><a>{item.name}</a></Link></h3>
<div className={styles['card-image']}>
{featuredImage && <img src={`/api/images/${featuredImage.uuid}/thumb`} />}
</div>

@ -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 (
<form autocomplete="off" onSubmit={handleSubmit} className={styles.formContainer}>
<form autoComplete="off" onSubmit={handleSubmit} className={styles.formContainer + (className?' ' + className:'')}>
{_children}
</form>
)

@ -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 (
<div className={styles.formElementContainer}>
<label htmlFor={name}>{label}:</label>
<div className={styles.complexInput + ((isValid && !error)?'':' ' + styles.invalid)}>
<button type="button" disabled={value===minimum} onClick={valueDown}>-</button>
<input type="number" name={name} value={value} onChange={onChange} onBlur={onBlur} />
<button type="button" disabled={value===maximum} onClick={valueUp}>+</button>
</div>
<span className={styles.hint}>{error || (isValid ? '' : hint)}</span>
</div>
)
}

@ -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;
}

@ -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,

@ -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 (
<>
<Header user={user} />
<main><Component {...{user, ...pageProps}} /></main>
<main><Component {...{user, cart, ...pageProps}} /></main>
<Footer/>
</>
)

@ -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 (
<div>
<p>Item: {slug}</p>
</div>
<>
<Head><title>{item.name}</title></Head>
<div className={styles.pageContainer}>
<div className={'dark ' + styles.itemDetails}>
{item.images && item.images.length > 0 && (
<div className={styles.imageContainer}>
<div className={styles.card}>
<img src={`/api/images/${item.images[selectedIndex].uuid}/thumb`} />
</div>
{item.images && item.images.length > 1 &&
<div className={styles.imageSelector}>
{item.images.map((image, index) => (
<img key={image.uuid}
onClick={()=>setSelected(index)}
className={index === selectedIndex ? styles.selectedImage : undefined}
src={`/api/images/${image.uuid}/thumb`}
/>
))}
</div>
}
</div>
)}
<div className={styles.controls}>
<h2>{item.name}</h2>
{ item.description.split('\n').map(p=>p.trim()).filter(p=>p !== '').map((p,i)=><p key={i}>{p}</p>) }
<FormController className={styles.form} url="/api/cart/add" afterSubmit={()=>Router.push('/store/cart')}>
<NumInput label="Add to cart" name="count" initialValue="1" minimum="1" validate={value=>isNum(value, {no_symbols: true})} hint="Enter the number of socks to add to your cart" />
<Button type="submit">Add to cart</Button>
</FormController>
<div className={styles.notes}>
<span>Price: ${(parseInt(item.price_cents) / 100).toFixed(2)} each</span>
<span>
{
item.number_in_stock
? item.number_in_stock + ' in stock'
: 'Out of stock'
}
</span>
<span>{numInCart} in cart</span>
</div>
</div>
</div>
<div className={styles.moreDetails}>
<h2>Additional Information</h2>
<p>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.</p>
<p>All purchases are shipped within a few days of your purchase, (USPS first class).</p>
</div>
</div>
</>
)
}

@ -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%;
}

@ -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{

Loading…
Cancel
Save