Improved number inputs and item edit form - no hookups
parent
b21ab94ac2
commit
44bca67092
@ -0,0 +1,92 @@
|
|||||||
|
import React, {useState, useRef} from 'react'
|
||||||
|
import useMeasure from 'use-measure'
|
||||||
|
|
||||||
|
import styles from './styles.module.css'
|
||||||
|
|
||||||
|
function truncateFixed(num, fixed) {
|
||||||
|
var re = new RegExp('^-?\\d+(?:\.\\d{0,' + (fixed || -1) + '})?');
|
||||||
|
return num.toString().match(re)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Input({label, prefix, numDecimals, error, hint, name, value, onChange, onBlur, isValid}){
|
||||||
|
const [currentDecimals, setCurrentDecimals] = useState((numDecimals === undefined || value === undefined || value === '') ? -1 : numDecimals)
|
||||||
|
const currentValue = value === undefined ? 0 : typeof value === 'number' ? value : typeof value === 'string' ? parseInt(value || '0', 10) : 0
|
||||||
|
|
||||||
|
const spanRef = useRef();
|
||||||
|
const {width} = useMeasure(spanRef);
|
||||||
|
|
||||||
|
const updateParent = value => onChange({target: {value}})
|
||||||
|
|
||||||
|
const onKeyDown = (ev) => {
|
||||||
|
const {key} = ev
|
||||||
|
switch(key) {
|
||||||
|
case 'Backspace':
|
||||||
|
setCurrentDecimals(dec => Math.max(dec - 1, -1))
|
||||||
|
if(currentDecimals > 0) {
|
||||||
|
updateParent(parseFloat(truncateFixed(currentValue, currentDecimals - 1)))
|
||||||
|
} else {
|
||||||
|
updateParent(Math.floor(currentValue / 10))
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '.':
|
||||||
|
setCurrentDecimals(0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '0':
|
||||||
|
case '1':
|
||||||
|
case '2':
|
||||||
|
case '3':
|
||||||
|
case '4':
|
||||||
|
case '5':
|
||||||
|
case '6':
|
||||||
|
case '7':
|
||||||
|
case '8':
|
||||||
|
case '9':
|
||||||
|
const digit = parseInt(key, 10)
|
||||||
|
// If we're still in the whole numbers
|
||||||
|
if(currentDecimals < 0) {
|
||||||
|
updateParent(currentValue * 10 + digit)
|
||||||
|
} else if (currentDecimals === 0) {
|
||||||
|
// Add decimal
|
||||||
|
setCurrentDecimals(dec => dec + 1)
|
||||||
|
updateParent(parseFloat(currentValue.toFixed(0) + '.' + digit))
|
||||||
|
} else if (currentDecimals < numDecimals) {
|
||||||
|
// Add to existing decimals
|
||||||
|
setCurrentDecimals(dec => dec + 1)
|
||||||
|
updateParent(parseFloat(currentValue.toFixed(currentDecimals) + digit))
|
||||||
|
} else {
|
||||||
|
// Can we play a tick noise of some sort?
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
const filledText = ((prefix + ' ') || '')
|
||||||
|
+ (Math.floor(currentValue) || '0')
|
||||||
|
+ (currentDecimals >= 0 ? '.' : '')
|
||||||
|
+ (currentDecimals > 0 ? currentValue.toFixed(currentDecimals).split('.')[1] : '')
|
||||||
|
|
||||||
|
const remainingText =
|
||||||
|
(currentDecimals < 0 ? '.' : '')
|
||||||
|
+ (numDecimals !== undefined && currentDecimals < numDecimals ? '0'.repeat(numDecimals - Math.max(currentDecimals, 0)) : '')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.formElementContainer}>
|
||||||
|
<label htmlFor={name}>{label}:</label>
|
||||||
|
<div className={styles.complexInput + ((isValid && !error)?'':' ' + styles.invalid)}>
|
||||||
|
<input style={{textIndent: width}} type="text" name={name} value="" onChange={()=>{}} onKeyDown={onKeyDown} onBlur={onBlur} />
|
||||||
|
<div className={styles.inputDecorators}>
|
||||||
|
<span ref={spanRef} className={styles.numFilled}>{filledText}</span>
|
||||||
|
<span className={styles.numRemaining}>{remainingText}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{hint && <span className={styles.hint}>{error || (isValid ? '' : hint)}</span>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {FormController, Input, DecimalInput, Button} from '~/components/form'
|
||||||
|
|
||||||
|
EditItem.getInitialProps = async ({ctx: {axios, query: {slug}}}) => {
|
||||||
|
const {data: item} = await axios.get(`/api/items/by-slug/${slug}`)
|
||||||
|
return {item}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EditItem({item}) {
|
||||||
|
const stringLengthAtLeastOne = str => str.length > 0
|
||||||
|
const slugRestrictions = str => {
|
||||||
|
if(str.length < 3) return false;
|
||||||
|
if(str.length > 20) return false;
|
||||||
|
if(!str.match(/^[-a-z0-9_]*$/i)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2>Editing {item.name}</h2>
|
||||||
|
<FormController afterSubmit={console.log}>
|
||||||
|
<Input name="name" initialValue={item.name} validate={stringLengthAtLeastOne} />
|
||||||
|
<Input name="description" initialValue={item.description} validate={stringLengthAtLeastOne} />
|
||||||
|
<Input label="URL Slug" name="urlslug" initialValue={item.urlslug} validate={slugRestrictions} />
|
||||||
|
<DecimalInput label="Price" name="price_cents" prefix="$" numDecimals={2} initialValue={item.price_cents / 100} transform={float => Math.floor(float * 100)} />
|
||||||
|
<Button type="submit">Save</Button>
|
||||||
|
</FormController>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
import React, {useState} from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import Table from '~/components/table'
|
||||||
|
|
||||||
|
Items.getInitialProps = async ({ctx}) => {
|
||||||
|
const {data: items} = await ctx.axios.get('/api/items?showUnpublished=true')
|
||||||
|
return {items}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Items({items: _items}){
|
||||||
|
const [items, setItems] = useState(_items)
|
||||||
|
|
||||||
|
const updateItem = newItem => setItems(items=>items.map(i => {
|
||||||
|
if(i.uuid === newItem.uuid)
|
||||||
|
return newItem
|
||||||
|
return i
|
||||||
|
}))
|
||||||
|
|
||||||
|
const publish = item => async () => {
|
||||||
|
const {data: newItem} = await axios.post(`/api/items/${item.uuid}/publish`)
|
||||||
|
updateItem(newItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
const unpublish = item => async () => {
|
||||||
|
const {data: newItem} = await axios.post(`/api/items/${item.uuid}/unpublish`)
|
||||||
|
updateItem(newItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2>Items</h2>
|
||||||
|
<Table
|
||||||
|
columns={[
|
||||||
|
{name: 'Name', extractor: item => item.name},
|
||||||
|
{name: 'URL', extractor: item =>
|
||||||
|
<Link href={`/store/item/${item.urlslug}`}><a>/store/item/{item.urlslug}</a></Link> },
|
||||||
|
{name: 'Price', extractor: item => `$${(item.price_cents / 100).toFixed(2)}`},
|
||||||
|
{name: 'Number in stock', extractor: item => item.number_in_stock},
|
||||||
|
{name: 'Actions', extractor: item => (
|
||||||
|
<span>
|
||||||
|
<Link href={`/admin/items/${item.urlslug}`}><a>Edit</a></Link>
|
||||||
|
{
|
||||||
|
item.published
|
||||||
|
?<button onClick={unpublish(item)} type="button" className="buttonLink">Unpublish</button>
|
||||||
|
:<button onClick={publish(item)} type="button" className="buttonLink">Publish</button>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
]}
|
||||||
|
rows={items}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue