1
0
Fork 0

Merge pull request #4585 from eriese/eriese-small-ui-fixes

(shared) UI tweaks plus comma decimal separators
This commit is contained in:
Joost De Cock 2023-07-27 11:46:17 +02:00 committed by GitHub
commit 1ce504b182
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 347 additions and 156 deletions

View file

@ -1,8 +1,9 @@
import { isDegreeMeasurement } from 'config/measurements.mjs' import { isDegreeMeasurement } from 'config/measurements.mjs'
import { measurementAsMm, formatMm } from 'shared/utils.mjs' import { measurementAsMm, formatMm, measurementAsUnits } from 'shared/utils.mjs'
import { Collapse } from 'shared/components/collapse.mjs' import { Collapse } from 'shared/components/collapse.mjs'
import { PlusIcon, EditIcon } from 'shared/components/icons.mjs' import { PlusIcon, EditIcon } from 'shared/components/icons.mjs'
import { useState } from 'react' import { NumberInput } from 'shared/components/workbench/menus/shared/inputs.mjs'
import { useState, useCallback } from 'react'
export const ns = ['account'] export const ns = ['account']
const Mval = ({ m, val = false, imperial = false, className = '' }) => const Mval = ({ m, val = false, imperial = false, className = '' }) =>
@ -100,35 +101,35 @@ export const MeasieInput = ({
stopLoading = () => null, stopLoading = () => null,
}) => { }) => {
const isDegree = isDegreeMeasurement(m) const isDegree = isDegreeMeasurement(m)
const factor = isDegree ? 1 : mset.imperial ? 25.4 : 10 const units = mset.imperial ? 'imperial' : 'metric'
const [val, setVal] = useState(() => {
const measie = mset.measies?.[m]
if (!measie) return ''
if (isDegree) return measie
return measurementAsUnits(measie, units)
})
const isValValid = (val) => const [valid, setValid] = useState(null)
typeof val === 'undefined' || val === '' ? null : val != false && !isNaN(val)
const isValid = (newVal) => (typeof newVal === 'undefined' ? isValValid(val) : isValValid(newVal))
const [val, setVal] = useState(mset.measies?.[m] / factor || '')
const [valid, setValid] = useState(isValid(mset.measies?.[m] / factor || ''))
// Update onChange // Update onChange
const update = (evt) => { const update = useCallback(
setVal(evt.target.value) (validVal, rawVal) => {
setValid(validVal)
setVal(validVal || rawVal)
const useVal = isDegree if (validVal && typeof onUpdate === 'function') {
? evt.target.value const useVal = isDegree ? validVal : measurementAsMm(validVal, units)
: measurementAsMm(evt.target.value, mset.imperial ? 'imperial' : 'metric')
const validUpdate = isValid(useVal)
setValid(validUpdate)
if (validUpdate && typeof onUpdate === 'function') {
onUpdate(m, useVal) onUpdate(m, useVal)
} }
} },
[isDegree, setValid, setVal, onUpdate, units, m]
)
const save = async () => { const save = async () => {
// FIXME // FIXME
startLoading() startLoading()
const measies = {} const measies = {}
measies[m] = val * factor measies[m] = isDegree ? val : measurementAsMm(val, units)
const result = await backend.updateSet(mset.id, { measies }) const result = await backend.updateSet(mset.id, { measies })
if (result.success) { if (result.success) {
refresh() refresh()
@ -137,7 +138,7 @@ export const MeasieInput = ({
stopLoading() stopLoading()
} }
const fraction = (i, base) => update({ target: { value: Math.floor(val) + i / base } }) const fraction = (i, base) => update(Math.floor(('' + val).split(/[\s.]/)[0]) + i / base)
if (!m) return null if (!m) return null
@ -147,48 +148,41 @@ export const MeasieInput = ({
<label className="shrink-0 grow max-w-full"> <label className="shrink-0 grow max-w-full">
{children} {children}
<span className="input-group"> <span className="input-group">
<input <NumberInput
type="number" className={`border-r-0 w-full`}
step={mset.imperial && !isDegree ? 0.03125 : 0.1}
className={`
input input-bordered text-base-content border-r-0 w-full
${valid === false && 'input-error'}
${valid === true && 'input-success'}
`}
value={val} value={val}
onChange={update} onUpdate={update}
onMount={setValid}
/> />
{mset.imperial ? (
<span <span
className={`bg-transparent border-y w-20 className={`bg-transparent border-y w-20
${valid === false && 'border-error text-neutral-content'} ${valid === false && 'border-error text-neutral-content'}
${valid === true && 'border-success text-neutral-content'} ${valid && 'border-success text-neutral-content'}
${valid === null && 'border-base-200 text-base-content'} ${valid === null && 'border-base-200 text-base-content'}
`} `}
> >
<Mval <Mval
imperial={true} imperial={mset.imperial}
val={val * 25.4} val={isDegree ? val : measurementAsMm(val, units)}
m={m} m={m}
className="text-base-content bg-transparent text-success text-xs font-bold p-0" className="text-base-content bg-transparent text-success text-xs font-bold p-0"
/> />
</span> </span>
) : null}
<span <span
role="img" role="img"
className={`bg-transparent border-y className={`bg-transparent border-y
${valid === false && 'border-error text-neutral-content'} ${valid === false && 'border-error text-neutral-content'}
${valid === true && 'border-success text-neutral-content'} ${valid && 'border-success text-neutral-content'}
${valid === null && 'border-base-200 text-base-content'} ${valid === null && 'border-base-200 text-base-content'}
`} `}
> >
{valid === true && '👍'} {valid && '👍'}
{valid === false && '🤔'} {valid === false && '🤔'}
</span> </span>
<span <span
className={`w-14 text-center className={`w-14 text-center
${valid === false && 'bg-error text-neutral-content'} ${valid === false && 'bg-error text-neutral-content'}
${valid === true && 'bg-success text-neutral-content'} ${valid && 'bg-success text-neutral-content'}
${valid === null && 'bg-base-200 text-base-content'} ${valid === null && 'bg-base-200 text-base-content'}
`} `}
> >

View file

@ -35,10 +35,14 @@ saveYourPattern: Save your pattern
giveItAName: Give it a name giveItAName: Give it a name
changeMeasies: Change Pattern Measurements changeMeasies: Change Pattern Measurements
editCurrentMeasies: Edit Current Measurements editCurrentMeasies: Edit Current Measurements
editCurrentMeasiesHeader: Edit Pattern Measurements
editCurrentMeasiesDesc: Changes you make here will not be saved to your measurements sets, and will only affect this pattern.
chooseNewSet: Choose a New Measurements Set chooseNewSet: Choose a New Measurements Set
weLackSomeMeasies: We lack { nr } measurements to create this pattern weLackSomeMeasies: We lack { nr } measurements to create this pattern
youCanPickOrEnter: You can either pick a measurements set, or enter them by hand, but we cannot proceed without these measurements. youCanPickOrEnter: You can either pick a measurements set, or enter them by hand, but we cannot proceed without these measurements.
measiesOk: We have all required measurements to create this pattern. measiesOk: We have all required measurements to create this pattern.
seeMissingMeasies: See missing measurements
appliedMeasies: We applied a new measurements set to this pattern.
exportForPrinting: Export for printing exportForPrinting: Export for printing
exportForEditing: Export for editing exportForEditing: Export for editing
exportAsData: Export as data exportAsData: Export as data

View file

@ -1,3 +1,4 @@
import { useCallback, useMemo } from 'react'
// Components // Components
import { OptionsIcon } from 'shared/components/icons.mjs' import { OptionsIcon } from 'shared/components/icons.mjs'
import { optionsMenuStructure, optionType } from 'shared/utils.mjs' import { optionsMenuStructure, optionType } from 'shared/utils.mjs'
@ -71,9 +72,17 @@ export const DesignOptions = ({
DynamicDocs = false, DynamicDocs = false,
}) => { }) => {
const menuNs = [`o_${design}`, ...ns] const menuNs = [`o_${design}`, ...ns]
const optionsMenu = optionsMenuStructure(patternConfig.options) const optionsMenu = useMemo(() => optionsMenuStructure(patternConfig.options), [patternConfig])
const getDocsPath = (option) => const updateFunc = useCallback(
`designs/${design}/options${option ? '/' + option.toLowerCase() : ''}` (name, value) => update.settings(['options', ...name], value),
[update]
)
// FIXME How do we find inherited docs?
const getDocsPath = useCallback(
(option) => `designs/${design}/options${option ? '/' + option.toLowerCase() : ''}`,
[design]
)
return ( return (
<WorkbenchMenu <WorkbenchMenu
@ -91,7 +100,7 @@ export const DesignOptions = ({
language, language,
ns: menuNs, ns: menuNs,
passProps: { settings, patternConfig }, passProps: { settings, patternConfig },
updateFunc: (name, value) => update.settings(['options', ...name], value), updateFunc,
}} }}
/> />
) )

View file

@ -1,5 +1,11 @@
import { useCallback, useMemo, useState, useEffect } from 'react' import { useCallback, useMemo, useState, useEffect, useRef } from 'react'
import { round, measurementAsMm, measurementAsUnits, formatFraction128 } from 'shared/utils.mjs' import {
round,
measurementAsMm,
measurementAsUnits,
formatFraction128,
fractionToDecimal,
} from 'shared/utils.mjs'
import { ChoiceButton } from 'shared/components/choice-button.mjs' import { ChoiceButton } from 'shared/components/choice-button.mjs'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
@ -12,8 +18,129 @@ import debounce from 'lodash.debounce'
* Inputs that deal with more specific use cases should wrap one of the above base inputs * Inputs that deal with more specific use cases should wrap one of the above base inputs
*******************************************************************************************/ *******************************************************************************************/
/** Regex to validate that an input is a number */
const numberInputMatchers = {
0: /^-?[0-9]*[.,eE]?[0-9]+$/, // match a single decimal separator
1: /^-?[0-9]*(\s?[0-9]+\/|[.,eE])?[0-9]+$/, // match a single decimal separator or fraction
}
/**
* Validate and parse a value that should be a number
* @param {any} val the value to validate
* @param {Boolean} allowFractions should fractions be considered valid input?
* @param {Number} min the minimum allowable value
* @param {Number} max the maximum allowable value
* @return {null|false|Number} null if the value is empty,
* false if the value is invalid,
* or the value parsed to a number if it is valid
*/
const validateVal = (val, allowFractions = true, min = -Infinity, max = Infinity) => {
// if it's empty, we're neutral
if (typeof val === 'undefined' || val === '') return null
// make sure it's a string
val = ('' + val).trim()
// get the appropriate match pattern and check for a match
const matchPattern = numberInputMatchers[Number(allowFractions)]
if (!val.match(matchPattern)) return false
// replace comma with period
const parsedVal = val.replace(',', '.')
// if fractions are allowed, parse for fractions, otherwise use the number as a value
const useVal = allowFractions ? fractionToDecimal(parsedVal) : parsedVal
// check that it's a number and it's in the range
if (isNaN(useVal) || useVal > max || useVal < min) return false
// all checks passed. return the parsed value
return useVal
}
/**
* A number input that accepts comma or period decimal separators.
* Because our use case is almost never going to include thousands, we're using a very simple way of accepting commas:
* The validator checks for the presence of a single comma or period followed by numbers
* The parser replaces a single comma with a period
*
* optionally accepts fractions
* @param {Number} options.val the value of the input
* @param {Function} options.onUpdate a function to handle when the value is updated to a valid value
* @param {Boolean} options.fractions should the input allow fractional input
*/
export const NumberInput = ({
value,
onUpdate,
onMount,
className,
fractions = true,
min = -Infinity,
max = Infinity,
}) => {
const valid = useRef(validateVal(value, fractions, min, max))
// this is the change handler that will be debounced by the debounce handler
// we check validity inside this debounced function because
// we need to call the debounce handler on change regardless of validity
// if we don't, the displayed value won't update
const handleChange = useCallback(
(newVal) => {
// only actually update if the value is valid
if (typeof onUpdate === 'function') {
onUpdate(valid.current, newVal)
}
},
[onUpdate, valid]
)
// get a debounce handler
const { debouncedHandleChange, displayVal } = useDebouncedHandlers({ handleChange, val: value })
// onChange
const onChange = useCallback(
(evt) => {
const newVal = evt.target.value
// set validity so it will display
valid.current = validateVal(newVal, fractions, min, max)
// handle the change
debouncedHandleChange(newVal)
},
[debouncedHandleChange, fractions, min, max, valid]
)
useEffect(() => {
if (typeof onMount === 'function') {
console.log('mount', valid.current)
onMount(valid)
}
}, [onMount])
return (
<input
type="text"
inputMode="number"
className={`input ${className || 'input-sm input-bordered grow text-base-content'}
${valid.current === false && 'input-error'}
${valid.current && 'input-success'}
`}
value={displayVal}
onChange={onChange}
/>
)
}
/** A component that shows a number input to edit a value */ /** A component that shows a number input to edit a value */
const EditCount = (props) => ( const EditCount = (props) => {
const { handleChange } = props
const onUpdate = useCallback(
(validVal) => {
if (validVal !== null && validVal !== false) handleChange(validVal)
},
[handleChange]
)
return (
<div className="form-control mb-2 w-full"> <div className="form-control mb-2 w-full">
<label className="label"> <label className="label">
<span className="label-text text-base-content">{props.min}</span> <span className="label-text text-base-content">{props.min}</span>
@ -21,18 +148,12 @@ const EditCount = (props) => (
<span className="label-text text-base-content">{props.max}</span> <span className="label-text text-base-content">{props.max}</span>
</label> </label>
<label className="input-group input-group-sm"> <label className="input-group input-group-sm">
<input <NumberInput value={props.current} onUpdate={onUpdate} min={props.min} max={props.max} />
type="number"
className={`
input input-sm input-bordered grow text-base-content
`}
value={props.current}
onChange={props.handleChange}
/>
<span className="text-base-content font-bold">#</span> <span className="text-base-content font-bold">#</span>
</label> </label>
</div> </div>
) )
}
/** /**
* A hook to get the change handler for an input. * A hook to get the change handler for an input.
@ -138,12 +259,12 @@ export const BoolInput = (props) => {
return <ListInput {...props} config={boolConfig} /> return <ListInput {...props} config={boolConfig} />
} }
export const useDebouncedHandlers = ({ handleChange, changed, current, config }) => { export const useDebouncedHandlers = ({ handleChange = () => {}, val }) => {
// hold onto what we're showing as the value so that the input doesn't look unresponsive // hold onto what we're showing as the value so that the input doesn't look unresponsive
const [displayVal, setDisplayVal] = useState(changed ? current : config.dflt) const [displayVal, setDisplayVal] = useState(val)
// the debounce function needs to be it's own memoized value so we can flush it on unmount // the debounce function needs to be it's own memoized value so we can flush it on unmount
const debouncer = useMemo(() => debounce(handleChange, 200), [handleChange]) const debouncer = useMemo(() => debounce(handleChange, 300), [handleChange])
// this is the change handler // this is the change handler
const debouncedHandleChange = useCallback( const debouncedHandleChange = useCallback(
@ -161,8 +282,8 @@ export const useDebouncedHandlers = ({ handleChange, changed, current, config })
// set the display val to the current value when it gets changed // set the display val to the current value when it gets changed
useEffect(() => { useEffect(() => {
setDisplayVal(changed ? current : config.dflt) setDisplayVal(val)
}, [changed, current, config]) }, [val])
return { debouncedHandleChange, displayVal } return { debouncedHandleChange, displayVal }
} }
@ -203,9 +324,7 @@ export const SliderInput = ({
const { debouncedHandleChange, displayVal } = useDebouncedHandlers({ const { debouncedHandleChange, displayVal } = useDebouncedHandlers({
handleChange, handleChange,
current, val: changed ? current : config.dflt,
changed,
config,
}) })
return ( return (
@ -216,7 +335,7 @@ export const SliderInput = ({
<EditCount <EditCount
{...{ {...{
current: displayVal, current: displayVal,
handleChange: (evt) => debouncedHandleChange(evt.target.value), handleChange,
min, min,
max, max,
t, t,
@ -254,20 +373,44 @@ export const SliderInput = ({
) )
} }
/**
* round a value to the correct number of decimal places to display all supplied digits after multiplication
* this is a workaround for floating point errors
* examples:
* roundPct(0.72, 100) === 72
* roundPct(7.5, 0.01) === 0.075
* roundPct(7.50, 0.01) === 0.0750
* @param {Number} num the number to be operated on
* @param {Number} factor the number to multiply by
* @return {Number} the given num multiplied by the factor, rounded appropriately
*/
const roundPct = (num, factor) => {
// stringify
const str = '' + num
// get the index of the decimal point in the number
const decimalIndex = str.indexOf('.')
// get the number of places the factor moves the decimal point
const factorPlaces = factor > 0 ? Math.ceil(Math.log10(factor)) : Math.floor(Math.log10(factor))
// the number of places needed is the number of digits that exist after the decimal minus the number of places the decimal point is being moved
const numPlaces = Math.max(0, str.length - (decimalIndex + factorPlaces))
return round(num * factor, numPlaces)
}
/** A {@see SliderInput} to handle percentage values */ /** A {@see SliderInput} to handle percentage values */
export const PctInput = ({ current, changed, updateFunc, config, ...rest }) => { export const PctInput = ({ current, changed, updateFunc, config, ...rest }) => {
const factor = 100 const factor = 100
let pctCurrent = changed ? current * factor : current let pctCurrent = changed ? roundPct(current, factor) : current
const pctUpdateFunc = useCallback( const pctUpdateFunc = useCallback(
(path, newVal) => updateFunc(path, newVal === undefined ? undefined : newVal / factor), (path, newVal) =>
[updateFunc, factor] updateFunc(path, newVal === undefined ? undefined : roundPct(newVal, 1 / factor)),
[updateFunc]
) )
return ( return (
<SliderInput <SliderInput
{...{ {...{
...rest, ...rest,
config: { ...config, dflt: config.dflt * factor }, config: { ...config, dflt: roundPct(config.dflt, factor) },
current: pctCurrent, current: pctCurrent,
updateFunc: pctUpdateFunc, updateFunc: pctUpdateFunc,
suffix: '%', suffix: '%',
@ -279,7 +422,16 @@ export const PctInput = ({ current, changed, updateFunc, config, ...rest }) => {
} }
/** A {@see SliderInput} to handle degree values */ /** A {@see SliderInput} to handle degree values */
export const DegInput = (props) => <SliderInput {...props} suffix="°" valFormatter={round} /> export const DegInput = (props) => {
const { updateFunc } = props
const degUpdateFunc = useCallback(
(path, newVal) => {
updateFunc(path, newVal === undefined ? undefined : Number(newVal))
},
[updateFunc]
)
return <SliderInput {...props} suffix="°" valFormatter={round} updateFunc={degUpdateFunc} />
}
export const MmInput = (props) => { export const MmInput = (props) => {
const { units, updateFunc, current, config } = props const { units, updateFunc, current, config } = props

View file

@ -1,5 +1,5 @@
// Hooks // Hooks
import { useEffect, useState, useCallback } from 'react' import { useEffect, useState, useCallback, useMemo } from 'react'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import { useView } from 'shared/hooks/use-view.mjs' import { useView } from 'shared/hooks/use-view.mjs'
import { usePatternSettings } from 'shared/hooks/use-pattern-settings.mjs' import { usePatternSettings } from 'shared/hooks/use-pattern-settings.mjs'
@ -103,32 +103,34 @@ export const Workbench = ({ design, Design, DynamicDocs }) => {
}, [Design, settings.measurements, mounted, view, setView]) }, [Design, settings.measurements, mounted, view, setView])
// Helper methods for settings/ui updates // Helper methods for settings/ui updates
const update = { const update = useMemo(
settings: (path, val) => setSettings(objUpdate({ ...settings }, path, val)), () => ({
ui: (path, val) => setUi(objUpdate({ ...ui }, path, val)), settings: (path, val) =>
setSettings((curSettings) => objUpdate({ ...curSettings }, path, val)),
ui: (path, val) => setUi((curUi) => objUpdate({ ...curUi }, path, val)),
toggleSa: () => { toggleSa: () => {
const sa = settings.samm || (account.imperial ? 15.3125 : 10) setSettings((curSettings) => {
if (settings.sabool) const sa = curSettings.samm || (account.imperial ? 15.3125 : 10)
setSettings(
objUpdate({ ...settings }, [ if (curSettings.sabool)
return objUpdate({ ...curSettings }, [
[['sabool'], 0], [['sabool'], 0],
[['sa'], 0], [['sa'], 0],
[['samm'], sa], [['samm'], sa],
]) ])
)
else { else {
const sa = settings.samm || (account.imperial ? 15.3125 : 10) return objUpdate({ ...curSettings }, [
setSettings(
objUpdate({ ...settings }, [
[['sabool'], 1], [['sabool'], 1],
[['sa'], sa], [['sa'], sa],
[['samm'], sa], [['samm'], sa],
]) ])
)
} }
})
}, },
setControl: controlState.update, setControl: controlState.update,
} }),
[setSettings, setUi, account, controlState]
)
// wait for mount. this helps prevent hydration issues // wait for mount. this helps prevent hydration issues
if (!mounted) return <ModalSpinner /> if (!mounted) return <ModalSpinner />

View file

@ -1,8 +1,12 @@
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import { analyzeDraftLogLine } from './errors.mjs' import { analyzeDraftLogLine } from './errors.mjs'
import Markdown from 'react-markdown' import Markdown from 'react-markdown'
import {
ClearAllButton,
ns as coreMenuNs,
} from 'shared/components/workbench/menus/core-settings/index.mjs'
export const ns = ['logs'] export const ns = ['logs', ...coreMenuNs]
const colors = { const colors = {
error: 'error', error: 'error',
@ -68,7 +72,7 @@ const extractLogs = (pattern) => {
return logs return logs
} }
export const LogView = ({ pattern, settings }) => { export const LogView = ({ pattern, settings, setSettings }) => {
const { t } = useTranslation(ns) const { t } = useTranslation(ns)
try { try {
@ -80,7 +84,10 @@ export const LogView = ({ pattern, settings }) => {
return ( return (
<div className="max-w-4xl mx-auto px-4 pb-8"> <div className="max-w-4xl mx-auto px-4 pb-8">
<h1>{t('logs')}</h1> <div className="flex">
<h1 className="grow">{t('logs')}</h1>
<ClearAllButton setSettings={setSettings} />
</div>
{Object.entries(logs).map(([type, lines], key) => ( {Object.entries(logs).map(([type, lines], key) => (
<DraftLogs key={key} {...{ type, lines, t }} /> <DraftLogs key={key} {...{ type, lines, t }} />
))} ))}

View file

@ -1,7 +1,7 @@
import { MeasieInput, ns as inputNs } from 'shared/components/sets/measie-input.mjs' import { MeasieInput, ns as inputNs } from 'shared/components/sets/measie-input.mjs'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
export const ns = ['wbmeasies', ...inputNs] export const ns = ['workbench', ...inputNs]
export const MeasiesEditor = ({ Design, settings, update }) => { export const MeasiesEditor = ({ Design, settings, update }) => {
const { t } = useTranslation(ns) const { t } = useTranslation(ns)
@ -13,6 +13,8 @@ export const MeasiesEditor = ({ Design, settings, update }) => {
return ( return (
<div> <div>
<h2>{t('editCurrentMeasiesHeader')}</h2>
<p>{t('editCurrentMeasiesDesc')}</p>
{Design.patternConfig.measurements.map((m) => ( {Design.patternConfig.measurements.map((m) => (
<MeasieInput {...{ t, m, mset, onUpdate }} key={m}> <MeasieInput {...{ t, m, mset, onUpdate }} key={m}>
<span className="label">{t(m)}</span> <span className="label">{t(m)}</span>

View file

@ -8,11 +8,11 @@ import { designMeasurements } from 'shared/utils.mjs'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import { useToast } from 'shared/hooks/use-toast.mjs' import { useToast } from 'shared/hooks/use-toast.mjs'
export const ns = ['wbmeasies', ...authNs, setsNs] export const ns = [...authNs, setsNs]
const tabNames = ['chooseNew', 'editCurrent'] const tabNames = ['chooseNewSet', 'editCurrentMeasies']
export const MeasiesView = ({ design, Design, settings, update, missingMeasurements, setView }) => { export const MeasiesView = ({ design, Design, settings, update, missingMeasurements, setView }) => {
const { t } = useTranslation(['wbmeasies']) const { t } = useTranslation(['workbench'])
const toast = useToast() const toast = useToast()
const tabs = tabNames.map((n) => t(n)).join(',') const tabs = tabNames.map((n) => t(n)).join(',')
@ -23,7 +23,7 @@ export const MeasiesView = ({ design, Design, settings, update, missingMeasureme
[['units'], set.imperial ? 'imperial' : 'metric'], [['units'], set.imperial ? 'imperial' : 'metric'],
]) ])
setView('draft') setView('draft')
toast.success(t('updatedMeasurements')) toast.success(t('appliedMeasies'))
} }
return ( return (

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useRef } from 'react'
import Head from 'next/head' import Head from 'next/head'
import { Header, ns as headerNs } from 'site/components/header/index.mjs' import { Header, ns as headerNs } from 'site/components/header/index.mjs'
import { Footer, ns as footerNs } from 'shared/components/footer/index.mjs' import { Footer, ns as footerNs } from 'shared/components/footer/index.mjs'
@ -16,23 +16,25 @@ export const LayoutWrapper = ({
slug, slug,
}) => { }) => {
const ChosenHeader = header ? header : Header const ChosenHeader = header ? header : Header
const prevScrollPos = useRef(0)
const [prevScrollPos, setPrevScrollPos] = useState(0)
const [showHeader, setShowHeader] = useState(true) const [showHeader, setShowHeader] = useState(true)
useEffect(() => { useEffect(() => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const handleScroll = () => { const handleScroll = () => {
const curScrollPos = typeof window !== 'undefined' ? window.pageYOffset : 0 const curScrollPos = typeof window !== 'undefined' ? window.pageYOffset : 0
if (curScrollPos >= prevScrollPos) {
if (showHeader && curScrollPos > 20) setShowHeader(false) if (curScrollPos >= prevScrollPos.current) {
if (curScrollPos > 20) setShowHeader(false)
} else setShowHeader(true) } else setShowHeader(true)
setPrevScrollPos(curScrollPos)
prevScrollPos.current = curScrollPos
} }
window.addEventListener('scroll', handleScroll) window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll) return () => window.removeEventListener('scroll', handleScroll)
} }
}, [prevScrollPos, showHeader]) }, [prevScrollPos, setShowHeader])
return ( return (
<div <div

View file

@ -151,40 +151,59 @@ export const getCrumbs = (app, slug = false) => {
/** convert a millimeter value to a Number value in the given units */ /** convert a millimeter value to a Number value in the given units */
export const measurementAsUnits = (mmValue, units = 'metric') => export const measurementAsUnits = (mmValue, units = 'metric') =>
mmValue / (units === 'imperial' ? 25.4 : 10) round(mmValue / (units === 'imperial' ? 25.4 : 10), 3)
/** convert a value that may contain a fraction to a decimal */
export const fractionToDecimal = (value) => {
// if it's just a number, return it
if (!isNaN(value)) return value
// keep a running total
let total = 0
// split by spaces
let chunks = String(value).split(' ')
if (chunks.length > 2) return Number.NaN // too many spaces to parse
// a whole number with a fraction
if (chunks.length === 2) {
// shift the whole number from the array
const whole = Number(chunks.shift())
// if it's not a number, return NaN
if (isNaN(whole)) return Number.NaN
// otherwise add it to the total
total += whole
}
// now we have only one chunk to parse
let fraction = chunks[0]
// split it to get numerator and denominator
let fChunks = fraction.trim().split('/')
// not really a fraction. return NaN
if (fChunks.length !== 2 || fChunks[1] === '') return Number.NaN
// do the division
let num = Number(fChunks[0])
let denom = Number(fChunks[1])
if (isNaN(num) || isNaN(denom)) return NaN
return total + num / denom
}
export const measurementAsMm = (value, units = 'metric') => { export const measurementAsMm = (value, units = 'metric') => {
if (typeof value === 'number') return value * (units === 'imperial' ? 25.4 : 10) if (typeof value === 'number') return value * (units === 'imperial' ? 25.4 : 10)
if (value.endsWith('.')) return false if (String(value).endsWith('.')) return false
if (units === 'metric') { if (units === 'metric') {
value = Number(value) value = Number(value)
if (isNaN(value)) return false if (isNaN(value)) return false
return value * 10 return value * 10
} else { } else {
const imperialFractionToMm = (value) => { const decimal = fractionToDecimal(value)
let chunks = value.trim().split('/') if (isNaN(decimal)) return false
if (chunks.length !== 2 || chunks[1] === '') return false return decimal * 24.5
let num = Number(chunks[0])
let denom = Number(chunks[1])
if (isNaN(num) || isNaN(denom)) return false
else return (num * 25.4) / denom
} }
let chunks = value.split(' ')
if (chunks.length === 1) {
let val = chunks[0]
if (!isNaN(Number(val))) return Number(val) * 25.4
else return imperialFractionToMm(val)
} else if (chunks.length === 2) {
let inches = Number(chunks[0])
if (isNaN(inches)) return false
let fraction = imperialFractionToMm(chunks[1])
if (fraction === false) return false
return inches * 25.4 + fraction
}
}
return false
} }
export const optionsMenuStructure = (options) => { export const optionsMenuStructure = (options) => {