// Dependencies
import {
capitalize,
cloudflareImageUrl,
measurementAsMm,
measurementAsUnits,
distanceAsMm,
validateEmail,
} from '@freesewing/utils'
import { collection } from '@freesewing/collection'
import { measurements as measurementsTranslations } from '@freesewing/i18n'
// Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks
import React, { useState, useCallback, useContext } from 'react'
import { useDropzone } from 'react-dropzone'
import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components
import { Link as WebLink } from '@freesewing/react/components/Link'
import { TrashIcon, ResetIcon, UploadIcon, HelpIcon } from '@freesewing/react/components/Icon'
import { isDegreeMeasurement } from '@freesewing/config'
import { Tabs, Tab } from '@freesewing/react/components/Tab'
import Markdown from 'react-markdown'
/*
* A helper component to render the help link in formcontrol
*
* @param {string|function| help - The help href of onClick method
* @param {React.component} Link - An optional framework-specific link component
*/
const HelpLink = ({ help, Link = false }) => {
if (!Link) Link = WebLink
if (typeof help === 'function')
return (
)
if (typeof help === 'string')
return (
)
return null
}
/**
* A component for a fieldset, which wraps form elements and providers labels.
*
* @component
* @param {object} props - All component props
* @param {React.Component} [props.Link = undefined] - A framework specific Link component for client-side routing
* @param {boolean} [props.box = undefined] - Set this to true to render a boxed fieldset
* @param {JSX.Element} props.children - The component children
* @param {string} [props.label = false] - The (top-left) label
* @param {string} [props.labelBL = false] - The bottom-left) label
* @param {string} [props.labelBR = false] - The bottom-right) label
* @param {string} [props.labelTR = false] - The top-right label
* @param {string} [props.legend = false] - The fieldset legend
* @param {string} [props.forId = ''] - Id of the HTML element we are wrapping
* @param {string|function} [props.help = false] - An optional URL/method to link/show help or docs
* @returns {JSX.Element}
*/
export const Fieldset = ({
Link = false,
box = false,
children,
label = false,
labelBL = false,
labelBR = false,
labelTR = false,
legend = false,
forId = '',
help = false,
}) => (
)
/**
* A component to wrap content in a button
*
* @component
* @param {object} props - All component props
* @param {boolean} [props.active = false] - Set this to true to render the button as active/selected
* @param {JSX.Element} props.children - The component children
* @param {boolean} [props.dense = false] - Set this to render a more compact variant
* @param {boolean} [props.noBg = false] - Set this to true to not use a background color in active state
* @param {function} props.onClick - The button's onClick handler
* @returns {JSX.Element}
*/
export const ButtonFrame = ({ active, children, dense, noBg, onClick }) => (
)
/**
* A component to handle input of numbers
*
* @component
* @param {object} props - All component props
* @param {boolean} [props.box = false] - Set this to true to render a boxed fieldset
* @param {number} props.current - The current value, to manage the state of this input
* @param {string|function} [props.help = false] - An optional URL/method to link/show help or docs
* @param {string} [props.inputMode = 'decimal'] - The inputMode of the input
* @param {string} [props.label = false] - The (top-left) label
* @param {string} [props.labelBL = false] - The bottom-left) label
* @param {string} [props.labelBR = false] - The bottom-right) label
* @param {string} [props.labelTR = false] - The top-right label
* @param {string} [props.id = ''] - Id of the HTML element to link the fieldset labels
* @param {string} [props.legend = false] - The fieldset legend
* @param {number} [props.max = 225] - The maximum value
* @param {number} [props.min = 0] - The minimum value
* @param {number} props.original - The original value, which detects whether it was changed
* @param {string} props.placeholder - The placeholder text
* @param {number} [props.step = 1] - The input step
* @param {function} props.update - The onChange handler
* @param {function} props.valid - A function that should return whether the value is valid or not
* @returns {JSX.Element}
*/
export const NumberInput = ({
box = false,
current,
help = false,
inputMode = 'decimal',
label = false,
labelBL = false,
labelBR = false,
labelTR = false,
id = '',
legend = false,
max = 225,
min = 0,
original,
placeholder,
step = 1,
update,
valid,
}) => (
)
/**
* A component to handle input of strings (single-line text)
*
* @component
* @param {object} props - All component props
* @param {boolean} [props.box = false] - Set this to true to render a boxed fieldset
* @param {number} props.current - The current value, to manage the state of this input
* @param {string|function} [props.help = false] - An optional URL/method to link/show help or docs
* @param {string} [props.label = false] - The (top-left) label
* @param {string} [props.labelBL = false] - The bottom-left) label
* @param {string} [props.labelBR = false] - The bottom-right) label
* @param {string} [props.labelTR = false] - The top-right label
* @param {string} [props.id = ''] - Id of the HTML element to link the fieldset labels
* @param {string} [props.legend = false] - The fieldset legend
* @param {number} props.original - The original value, which detects whether it was changed
* @param {string} props.placeholder - The placeholder text
* @param {function} props.update - The onChange handler
* @param {function} [props.valid = () => true] - A function that should return whether the value is valid or not
* @returns {JSX.Element}
*/
export const StringInput = ({
box = false,
current,
help = false,
label = false,
labelBL = false,
labelBR = false,
labelTR = false,
id = '',
legend = false,
original,
placeholder,
update,
valid = () => true,
}) => (
)
/**
* A component to handle input of MFA codes. Essentially a NumberInput with some default props set.
*
* @component
* @param {object} props - All component props
* @param {boolean} [props.box = false] - Set this to true to render a boxed fieldset
* @param {number} props.current - The current value, to manage the state of this input
* @param {string|function} [props.help = false] - An optional URL/method to link/show help or docs
* @param {string} [props.label = false] - The (top-left) label
* @param {string} [props.labelBL = false] - The bottom-left) label
* @param {string} [props.labelBR = false] - The bottom-right) label
* @param {string} [props.labelTR = false] - The top-right label
* @param {string} [props.id = 'mfa'] - Id of the HTML element to link the fieldset labels
* @param {string} [props.inputMode = 'numeric'] - The input mode of the input
* @param {string} [props.legend = false] - The fieldset legend
* @param {string} [props.placeholder = 'MFA Code'] - The placeholder text
* @param {function} props.update - The onChange handler
* @param {function} props.valid - A function that should return whether the value is valid or not
* @returns {JSX.Element}
*/
export const MfaInput = ({
box = false,
current,
help = false,
label = false,
labelBL = false,
labelBR = false,
labelTR = false,
id = 'mfa',
inputMode = 'numeric',
legend = false,
placeholder = 'MFA Code',
update,
valid = (val) => val.length > 4,
}) => (
)
/**
* A component to handle input of passwords
*
* @component
* @param {object} props - All component props
* @param {boolean} [props.box = false] - Set this to true to render a boxed fieldset
* @param {number} props.current - The current value, to manage the state of this input
* @param {string|function} [props.help = false] - An optional URL/method to link/show help or docs
* @param {string} [props.label = false] - The (top-left) label
* @param {string} [props.labelBL = false] - The bottom-left) label
* @param {string} [props.labelTR = false] - The top-right label
* @param {string} [props.id = 'password'] - Id of the HTML element to link the fieldset labels
* @param {string} [props.legend = false] - The fieldset legend
* @param {string} [placeholder = '¯\\_(ツ)_/¯' - The placeholder text
* @param {function} props.update - The onChange handler
* @param {function} [props.valid = () => true] - A function that should return whether the value is valid or not
* @param {function} [props.onKeyDown = false] - An optional handler to capture keypresses (like enter)
* @returns {JSX.Element}
*/
export const PasswordInput = ({
box = false,
current,
help = false,
label = false,
labelBL = false,
labelTR = false,
id = 'password',
legend = false,
placeholder = '¯\\_(ツ)_/¯',
update,
valid = () => true,
onKeyDown = false,
}) => {
const [reveal, setReveal] = useState(false)
const extraProps = onKeyDown ? { onKeyDown } : {}
return (
)
}
/**
* A component to handle input of email addresses
*
* @component
* @param {object} props - All component props
* @param {boolean} [props.box = false] - Set this to true to render a boxed fieldset
* @param {number} props.current - The current value, to manage the state of this input
* @param {string|function} [props.help = false] - An optional URL/method to link/show help or docs
* @param {string} [props.id = ''] - Id of the HTML element to link the fieldset labels
* @param {string} [props.label = false] - The (top-left) label
* @param {string} [props.labelBL = false] - The bottom-left) label
* @param {string} [props.labelBR = false] - The bottom-right) label
* @param {string} [props.labelTR = false] - The top-right label
* @param {string} [props.legend = false] - The fieldset legend
* @param {number} [props.original = ''] - The original value, which detects whether it was changed
* @param {string} [props.placeholder = 'Email Address'] - The placeholder text
* @param {function} props.update - The onChange handler
* @param {function} [props.valid = @freesewing/utils.validateEmail] - A function that should return whether the value is valid or not
* @returns {JSX.Element}
*/
export const EmailInput = ({
box = false,
current,
help = false,
id = 'email',
label = false,
labelBL = false,
labelBR = false,
labelTR = false,
legend = false,
original = '',
update,
placeholder = 'Email Address',
valid = validateEmail,
}) => (
)
/**
* A component to handle input of a design name (a select)
*
* @component
* @param {object} props - All component props
* @param {boolean} [props.box = false] - Set this to true to render a boxed fieldset
* @param {number} props.current - The current value, to manage the state of this input
* @param {string} [props.firstOption = false] - An optional first option to add to the select
* @param {string|function} [props.help = false] - An optional URL/method to link/show help or docs
* @param {string} [props.id = 'design'] - Id of the HTML element to link the fieldset labels
* @param {string} [props.label = false] - The (top-left) label
* @param {string} [props.labelBL = false] - The bottom-left) label
* @param {string} [props.labelBR = false] - The bottom-right) label
* @param {string} [props.labelTR = false] - The top-right label
* @param {string} [props.legend = false] - The fieldset legend
* @param {function} props.update - The onChange handler
* @returns {JSX.Element}
*/
export const DesignInput = ({
box = false,
current,
firstOption = false,
help = false,
id = 'design',
label = false,
labelBL = false,
labelBR = false,
labelTR = false,
legend = false,
update,
}) => (
)
/**
* A component to handle input of an image
*
* @component
* @param {object} props - All component props
* @param {boolean} [props.active = false] - Set this to true to automatically upload the image
* @param {boolean} [props.box = false] - Set this to true to render a boxed fieldset
* @param {number} props.current - The current value, to manage the state of this input
* @param {string|function} [props.help = false] - An optional URL/method to link/show help or docs
* @param {string} [props.id = 'image'] - Id of the HTML element to link the fieldset labels
* @param {string} [props.imgType = 'showcase'] - The type of image. One of 'showcase' or 'blog'
* @param {string} props.imgSlug - The slug of the image, which is the foldername holding the blog or showcase post
* @param {string} props.imgSubid - Set this id to upload non-main images, should be unique per post (1,2,3,...)
* @param {string} [props.label = false] - The (top-left) label
* @param {string} [props.labelBL = false] - The bottom-left) label
* @param {string} [props.labelBR = false] - The bottom-right) label
* @param {string} [props.labelTR = false] - The top-right label
* @param {string} [props.legend = false] - The fieldset legend
* @param {number} props.original - The original value, which allows a reset
* @param {function} props.update - The onChange handler
* @returns {JSX.Element}
*/
export const ImageInput = ({
active = false,
box = false,
current,
help = false,
id = 'image',
imgSlug,
imgSubid,
imgType = 'showcase',
label = false,
labelBL = false,
labelBR = false,
labelTR = false,
legend = false,
update,
original,
}) => {
const backend = useBackend()
const { setLoadingStatus } = useContext(LoadingStatusContext)
const [url, setUrl] = useState(false)
const [uploadedId, setUploadedId] = useState(false)
const upload = async (img, fromUrl = false) => {
setLoadingStatus([true, 'uploadingImage'])
const data = {
type: imgType,
subId: imgSubid,
slug: imgSlug,
}
if (fromUrl) data.url = img
else data.img = img
const [status, body] = await backend.uploadImageAnon(data)
setLoadingStatus([true, 'allDone', true, true])
if (status === 200 && body.result === 'success') {
update(body.imgId)
setUploadedId(body.imgId)
} else setLoadingStatus([true, 'backendError', true, false])
}
const onDrop = useCallback(
(acceptedFiles) => {
const reader = new FileReader()
reader.onload = async () => {
if (active) upload(reader.result)
else update(reader.result)
}
acceptedFiles.forEach((file) => reader.readAsDataURL(file))
},
[current]
)
const { getRootProps, getInputProps } = useDropzone({ onDrop })
if (current)
return (
)
return (
)
}
/**
* A component to handle input of an image and upload it (active)
*
* @component
* @param {object} props - All component props
* @param {boolean} [props.box = false] - Set this to true to render a boxed fieldset
* @param {number} props.current - The current value, to manage the state of this input
* @param {string|function} [props.help = false] - An optional URL/method to link/show help or docs
* @param {string} [props.id = 'image'] - Id of the HTML element to link the fieldset labels
* @param {string} [props.imgType = 'showcase'] - The type of image. One of 'showcase' or 'blog'
* @param {string} props.imgSlug - The slug of the image, which is the foldername holding the blog or showcase post
* @param {string} props.imgSubid - Set this id to upload non-main images, should be unique per post (1,2,3,...)
* @param {string} [props.label = false] - The (top-left) label
* @param {string} [props.labelBL = false] - The bottom-left) label
* @param {string} [props.labelBR = false] - The bottom-right) label
* @param {string} [props.labelTR = false] - The top-right label
* @param {string} [props.legend = false] - The fieldset legend
* @param {number} props.original - The original value, which allows a reset
* @param {function} props.update - The onChange handler
* @returns {JSX.Element}
*/
export const ActiveImageInput = (props) =>
/**
* A component to handle input of an image and not upload it (inactive)
*
* @component
* @param {object} props - All component props
* @param {boolean} [props.box = false] - Set this to true to render a boxed fieldset
* @param {number} props.current - The current value, to manage the state of this input
* @param {string|function} [props.help = false] - An optional URL/method to link/show help or docs
* @param {string} [props.id = 'image'] - Id of the HTML element to link the fieldset labels
* @param {string} [props.imgType = 'showcase'] - The type of image. One of 'showcase' or 'blog'
* @param {string} props.imgSlug - The slug of the image, which is the foldername holding the blog or showcase post
* @param {string} props.imgSubid - Set this id to upload non-main images, should be unique per post (1,2,3,...)
* @param {string} [props.label = false] - The (top-left) label
* @param {string} [props.labelBL = false] - The bottom-left) label
* @param {string} [props.labelBR = false] - The bottom-right) label
* @param {string} [props.labelTR = false] - The top-right label
* @param {string} [props.legend = false] - The fieldset legend
* @param {number} props.original - The original value, which allows a reset
* @param {function} props.update - The onChange handler
* @returns {JSX.Element}
*/
export const PassiveImageInput = (props) =>
/**
* A component to handle input of list of items to pick from
*
* @component
* @param {object} props - All component props
* @param {boolean} [props.box = false] - Set this to true to render a boxed fieldset
* @param {number} props.current - The current value, to manage the state of this input
* @param {string|function} [props.help = false] - An optional URL/method to link/show help or docs
* @param {string} [props.id = ''] - Id of the HTML element to link the fieldset labels
* @param {string} [props.label = false] - The (top-left) label
* @param {string} [props.labelBL = false] - The bottom-left) label
* @param {string} [props.labelBR = false] - The bottom-right) label
* @param {string} [props.labelTR = false] - The top-right label
* @param {string} [props.legend = false] - The fieldset legend
* @param {array} props.list - An array of { val, label, desc } objects to populate the list
* @param {function} props.update - The onChange handler
* @returns {JSX.Element}
*/
export const ListInput = ({
box = false,
current,
help = false,
id = '',
label = false,
labelBL = false,
labelBR = false,
labelTR = false,
legend = false,
list,
update,
}) => (
)
/**
* A component to handle input of markdown content
*
* @component
* @param {object} props - All component props
* @param {boolean} [props.box = false] - Set this to true to render a boxed fieldset
* @param {number} props.current - The current value, to manage the state of this input
* @param {string|function} [props.help = false] - An optional URL/method to link/show help or docs
* @param {string} [props.id = ''] - Id of the HTML element to link the fieldset labels
* @param {string} [props.label = false] - The (top-left) label
* @param {string} [props.labelBL = 'This field supports markdown'] - The bottom-left) label
* @param {string} [props.labelBR = false] - The bottom-right) label
* @param {string} [props.labelTR = false] - The top-right label
* @param {string} [props.legend = false] - The fieldset legend
* @param {function} props.update - The onChange handler
* @param {string} [props.placeholder = ''] - The placeholder text
* @returns {JSX.Element}
*/
export const MarkdownInput = ({
box = false,
current,
help = false,
id = '',
label = false,
labelBL = 'This field supports markdown',
labelBR = false,
labelTR = false,
legend = false,
update,
placeholder = '',
}) => (
)
/**
* A component to handle input of markdown content
*
* @component
* @param {object} props - All component props
* @param {boolean} [props.box = false] - Set this to true to render a boxed fieldset
* @param {string|function} [props.help = false] - An optional URL/method to link/show help or docs
* @param {string} [props.id = ''] - Id of the HTML element to link the fieldset labels
* @param {boolean} [props.imperial = false] - Set this to true to render imperial units
* @param {string} [props.labelBR = false] - The bottom-right) label
* @param {string} [props.labelTR = false] - The top-right label
* @param {string} [props.legend = false] - The fieldset legend
* @param {string} props.m - The measurement ID (name)
* @param {function} props.update - The onChange handler
* @param {number} props.original - The original value, which allows a reset
* @param {string} [props.placeholder = ''] - The placeholder text
* @returns {JSX.Element}
*/
export const MeasurementInput = ({
box = false,
help = false,
id = '',
imperial = false,
labelBR = false,
labelTR = false,
legend = false,
m,
update,
original,
placeholder = '',
}) => {
const isDegree = isDegreeMeasurement(m)
const units = imperial ? 'imperial' : 'metric'
const [localVal, setLocalVal] = useState(
typeof original === 'undefined'
? original
: isDegree
? Number(original)
: measurementAsUnits(original, units)
)
const [validatedVal, setValidatedVal] = useState(measurementAsUnits(original, units))
const [valid, setValid] = useState(null)
// Update onChange
const localUpdate = (newVal) => {
setLocalVal(newVal)
const parsedVal = isDegree ? Number(newVal) : distanceAsMm(newVal, imperial)
if (parsedVal) {
update(m, isDegree ? parsedVal : measurementAsMm(parsedVal, units))
setValid(true)
setValidatedVal(parsedVal)
} else if (newVal === undefined) update(m, undefined)
else setValid(false)
}
// Clear value
const clearValue = () => {
localUpdate(undefined)
setLocalVal('')
}
if (!m) return null
// Various visual indicators for validating the input
let inputClasses = 'daisy-input-secondary'
let bottomLeftLabel = null
if (valid === true) {
inputClasses = 'daisy-input-success tw:outline-success'
const val = `${validatedVal}${isDegree ? '°' : imperial ? '"' : 'cm'}`
bottomLeftLabel = (
{val}
)
} else if (valid === false) {
inputClasses = 'daisy-input-error'
bottomLeftLabel = (
¯\_(ツ)_/¯
)
}
/*
* I'm on the fence here about using a text input rather than number
* Obviously, number is the more correct option, but when the user enter
* text, it won't fire an onChange event and thus they can enter text and it
* will not be marked as invalid input.
* See: https://github.com/facebook/react/issues/16554
*/
return (
)
}
/**
* A component to handle input of file (upload)
*
* @component
* @param {object} props - All component props
* @param {boolean} [props.box = false] - Set this to true to render a boxed fieldset
* @param {number} props.current - The current value, to manage the state of this input
* @param {number} [props.dropzoneConfig = {}] - The configuration for react-dropzone
* @param {string|function} [props.props.help = false] - An optional URL/method to link/show help or docs
* @param {string} [props.id = ''] - Id of the HTML element to link the fieldset labels
* @param {string} [props.label = false] - The label
* @param {string} [props.labelBL = false] - The bottom-left) label
* @param {string} [props.labelBR = false] - The bottom-right) label
* @param {string} [props.labelTR = false] - The top-right label
* @param {string} [props.legend = false] - The fieldset legend
* @param {function} props.update - The onChange handler
* @param {number} props.original - The original value, which allows a reset
* @returns {JSX.Element}
*/
export const FileInput = ({
box = false,
current,
dropzoneConfig = {},
help = false,
id = '',
label = false,
labelBL = false,
labelBR = false,
labelTR = false,
legend = false,
update,
original,
}) => {
/*
* Ondrop handler
*/
const onDrop = useCallback(
(acceptedFiles) => {
const reader = new FileReader()
reader.onload = async () => update(reader.result)
acceptedFiles.forEach((file) => reader.readAsDataURL(file))
},
[update]
)
/*
* Dropzone hook
*/
const { getRootProps, getInputProps } = useDropzone({ onDrop, ...dropzoneConfig })
/*
* If we have a current file, return this
*/
if (current)
return (
)
/*
* Return upload form
*/
return (
)
}
/*
* Input for booleans
*/
/**
* A component to handle input of booleans (yes/no or on/off)
*
* @component
* @param {object} props - All component props
* @param {boolean} [props.box = false] - Set this to true to render a boxed fieldset
* @param {number} props.current - The current value, to manage the state of this input
* @param {boolean} [props.disabled = false] - Set this to true to render a disabled input
* @param {string|function} [props.help = false] - An optional URL/method to link/show help or docs
* @param {string} [props.id = ''] - Id of the HTML element to link the fieldset labels
* @param {string} [props.label = false] - The label
* @param {string} [props.labelBL = false] - The bottom-left) label
* @param {string} [props.labelBR = false] - The bottom-right) label
* @param {string} [props.labelTR = false] - The top-right label
* @param {array} [props.labels = ['Yes', 'No'] - An array of labels for the values
* @param {string} [props.legend = false] - The fieldset legend
* @param {array} [props.list = [true, false] - An array of values to choose between
* @param {function} props.update - The onChange handler
* @param {any} [props.on = true] - The value that should show the toggle in the 'on' state
* @returns {JSX.Element}
*/
export const ToggleInput = ({
box = false,
current,
disabled = false,
help = false,
id = '',
label = false,
labelBL = false,
labelBR = false,
labelTR = false,
labels = ['Yes', 'No'],
legend = false,
list = [true, false],
update,
on = true,
}) => (
)