Item page - rudimentary cart hookups
parent
8862e5dac0
commit
eeee3c3d49
@ -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)
|
||||||
|
})
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
@ -1,17 +1,82 @@
|
|||||||
import React from 'react'
|
import React, {useState} from 'react'
|
||||||
import {useRouter} from 'next/router'
|
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}){
|
import styles from './slug.module.css'
|
||||||
return {}
|
|
||||||
|
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(){
|
// TODO: Modal with full image size on clicking preview
|
||||||
const router = useRouter()
|
export default function Item({item, cart}){
|
||||||
const {slug} = router.query
|
// 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 (
|
return (
|
||||||
<div>
|
<>
|
||||||
<p>Item: {slug}</p>
|
<Head><title>{item.name}</title></Head>
|
||||||
</div>
|
<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%;
|
||||||
|
}
|
Loading…
Reference in New Issue