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