From 0f30c7396486f562b6604fc9bec3a135bd54d0f1 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Thu, 5 Jan 2023 20:46:50 -0800 Subject: [PATCH] Implement SelectWithInput We keep making this pattern so lets standardize it --- components/SelectWithInput/index.scss | 30 ++++ components/SelectWithInput/index.tsx | 199 ++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 components/SelectWithInput/index.scss create mode 100644 components/SelectWithInput/index.tsx diff --git a/components/SelectWithInput/index.scss b/components/SelectWithInput/index.scss new file mode 100644 index 00000000..036a0af8 --- /dev/null +++ b/components/SelectWithInput/index.scss @@ -0,0 +1,30 @@ +.SelectWithItem { + .InputSet { + display: flex; + flex-direction: row; + gap: $unit; + width: 100%; + + .SelectTrigger { + flex-grow: 1; + width: 100%; + } + + .Input { + // min-width: $unit * 12; + flex-grow: 0; + text-align: right; + width: inherit; + } + } + + .errors { + color: $error; + display: none; + padding: $unit 0; + + &.visible { + display: block; + } + } +} diff --git a/components/SelectWithInput/index.tsx b/components/SelectWithInput/index.tsx new file mode 100644 index 00000000..99f638a1 --- /dev/null +++ b/components/SelectWithInput/index.tsx @@ -0,0 +1,199 @@ +// Core dependencies +import React, { useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import { useTranslation } from 'next-i18next' +import classNames from 'classnames' + +// UI Dependencies +import Input from '~components/Input' +import Select from '~components/Select' +import SelectItem from '~components/SelectItem' + +// Styles and icons +import './index.scss' + +// Types +interface Props { + object: 'ax' | 'weapon_awakening' | 'character_awakening' | 'ring' | 'earring' + dataSet: ItemSkill[] + selectValue: number + selectDisabled: boolean + inputValue: number + awakeningLevel?: number + onOpenChange: (open: boolean) => void + sendValidity: (isValid: boolean) => void + sendValues: (type: number, level: number) => void +} + +const defaultProps = { + selectDisabled: false, +} + +const SelectWithInput = ({ + object, + dataSet, + selectDisabled, + selectValue, + inputValue, + onOpenChange, + sendValidity, + sendValues, +}: Props) => { + const router = useRouter() + const locale = + router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' + const { t } = useTranslation('common') + + // UI state + const [open, setOpen] = useState(false) + + // Field properties + // prettier-ignore + const [currentItemSkill, setCurrentItemSkill] = useState(undefined) + const [fieldInputValue, setFieldInputValue] = useState(inputValue) + + const [error, setError] = useState('') + + // Refs + const input = React.createRef() + + // Classes + const inputClasses = classNames({ + Bound: true, + Hidden: currentItemSkill?.id === 0, + }) + + const errorClasses = classNames({ + errors: true, + visible: error !== '', + }) + + // Hooks + + // Set default values from props + useEffect(() => { + setCurrentItemSkill(dataSet.find((sk) => sk.id === selectValue)) + setFieldInputValue(inputValue) + }, [selectValue, inputValue]) + + // Methods: UI state management + function changeOpen() { + if (!selectDisabled) { + setOpen(!open) + onOpenChange(!open) + } + } + + function onClose() { + onOpenChange(false) + } + + // Methods: Rendering + function generateOptions() { + let options: React.ReactNode[] = dataSet.map((skill, i) => { + return ( + + {skill.name[locale]} + + ) + }) + + if (object === 'weapon_awakening') { + options?.unshift( + + {t(`${object}.no_type`)} + + ) + } + + return options + } + + // Methods: User input detection + function handleSelectChange(rawValue: string) { + const value = parseInt(rawValue) + const skill = dataSet.find((sk) => sk.id === value) + setCurrentItemSkill(skill) + } + + function handleInputChange(event: React.ChangeEvent) { + const value = parseFloat(event.target.value) + if (handleInputError(value)) setFieldInputValue(value) + } + + // Methods: Handle error + function handleInputError(value: number) { + let error = '' + + if (currentItemSkill) { + if (value < currentItemSkill.minValue) { + error = t(`${object}.errors.value_too_low`, { + minValue: currentItemSkill.minValue, + }) + } else if (value > currentItemSkill.maxValue) { + error = t(`${object}.errors.value_too_high`, { + maxValue: currentItemSkill.maxValue, + }) + } else if (!currentItemSkill.fractional && value % 1 != 0) { + error = t(`${object}.errors.value_not_whole`) + } else if (!value || value <= 0) { + error = t(`${object}.errors.value_empty`) + } else { + error = '' + } + } + + setError(error) + + return error.length === 0 + } + + const rangeString = () => { + let placeholder = '' + + if (currentItemSkill) { + const minValue = currentItemSkill.minValue + const maxValue = currentItemSkill.maxValue + placeholder = `${minValue}~${maxValue}` + } + + return placeholder + } + + return ( +
+
+ + + +
+

{error}

+
+ ) +} + +SelectWithInput.defaultProps = defaultProps + +export default SelectWithInput