hensei-web/components/mastery/AwakeningSelectWithInput/index.tsx
Justin Edmund a02a6c70aa
Jedmund/image embeds 2 (#424)
## Component Refactors:
- Updated `CharacterHovercard` to improve over mastery and awakening
section logic.
- Refactored `CharacterModal` to streamline state management (rings,
awakening, perpetuity) and object preparation.
- Adjusted `CharacterUnit` for consistent over mastery handling.
- Simplified `AwakeningSelectWithInput` to use awakening slug values and
improve error handling.
- Updated `RingSelect` to refine ring value syncing and index logic.
- Modified `Party` and `PartyHead` to ensure consistent over mastery
processing and proper preview URL construction.
- Updated `WeaponModal` to align awakening value handling with the new
slug-based approach.

## Styling and Configuration:
- Improved grid layout and styling in the `WeaponRep` SCSS module.
- Updated `next.config.js` rewrite rules to support new preview and
character routes.
- Added a new API endpoint (`pages/api/preview/[shortcode].tsx`) for
fetching party preview images.

## Type Definitions:
- Refined types in `types/GridCharacter.d.ts` and `types/index.d.ts` to
reflect updated structures for rings, over mastery, and awakening.
2025-02-09 22:54:15 -08:00

217 lines
5.5 KiB
TypeScript

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/common/Input'
import Select from '~components/common/Select'
import SelectItem from '~components/common/SelectItem'
// Styles and icons
import styles from './index.module.scss'
// Types
interface Props {
dataSet: Awakening[]
defaultAwakening: Awakening
awakening?: Awakening
level?: number
maxLevel: number
selectDisabled: boolean
onOpenChange?: (open: boolean) => void
sendValidity: (isValid: boolean) => void
sendValues: (type: string, level: number) => void
}
const defaultProps = {
selectDisabled: false,
}
const AwakeningSelectWithInput = ({
dataSet,
defaultAwakening,
awakening,
level,
maxLevel,
selectDisabled,
onOpenChange,
sendValidity,
sendValues,
}: Props) => {
// Set up translations
const router = useRouter()
const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const { t } = useTranslation('common')
// State: Component
const [open, setOpen] = useState(false)
const [error, setError] = useState('')
// State: Data
const [currentAwakening, setCurrentAwakening] = useState<Awakening>()
const [currentLevel, setCurrentLevel] = useState(1)
// Refs
const inputRef = React.createRef<HTMLInputElement>()
// Classes
const inputClasses = classNames({
fullHeight: true,
range: true,
})
const errorClasses = classNames({
[styles.errors]: true,
[styles.visible]: error !== '',
})
// Hooks
useEffect(() => {
setCurrentAwakening(awakening)
setCurrentLevel(level ? level : 1)
// If there is an awakening (even if it's the default) we consider the field valid.
if (awakening || defaultAwakening) sendValidity(true)
}, [])
// Methods: UI state management
function changeOpen() {
if (!selectDisabled) {
setOpen(!open)
if (onOpenChange) onOpenChange(!open)
}
}
function onClose() {
if (onOpenChange) onOpenChange(false)
}
// Methods: Rendering
function generateOptions() {
const sortedDataSet = [...dataSet].sort((a, b) => a.order - b.order)
let options: React.ReactNode[] = sortedDataSet.map((awakening) =>
generateItem(awakening)
)
if (!dataSet.includes(defaultAwakening))
options.unshift(generateItem(defaultAwakening))
return options
}
function generateItem(awakening: Awakening) {
return (
<SelectItem key={awakening.slug} value={awakening.slug}>
{awakening.name[locale]}
</SelectItem>
)
}
// Methods: User input detection
function handleSelectChange(value: string) {
// Here, value is the awakening slug.
const input = inputRef.current
if (input && !handleInputError(parseFloat(input.value))) return
const selectedAwakening = dataSet.find((a) => a.slug === value)
setCurrentAwakening(selectedAwakening)
sendValues(value, currentLevel)
}
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
const input = inputRef.current
if (input && !handleInputError(parseFloat(input.value))) return
const newLevel = parseInt(event.target.value)
setCurrentLevel(newLevel)
sendValues(
currentAwakening ? currentAwakening.slug : defaultAwakening.slug,
newLevel
)
}
// Methods: Handle error
function handleInputError(value: number) {
let error = ''
if (currentAwakening) {
if (value && value % 1 !== 0) {
error = t(`awakening.errors.value_not_whole`)
} else if (value < 1) {
error = t(`awakening.errors.value_too_low`, { minValue: 1 })
} else if (value > maxLevel) {
error = t(`awakening.errors.value_too_high`, { maxValue: maxLevel })
} else if (!value || value <= 0) {
error = t(`awakening.errors.value_empty`)
} else {
error = ''
}
}
setError(error)
if (error.length > 0) {
sendValidity(false)
return false
} else {
sendValidity(true)
return true
}
}
const rangeString = () => {
let placeholder = ''
if (currentAwakening) {
placeholder = `1~${maxLevel}`
}
return placeholder
}
return (
<div>
<div className={styles.set}>
<Select
key="awakening-type"
// Use the slug as the value
value={`${awakening ? awakening.slug : defaultAwakening.slug}`}
open={open}
disabled={selectDisabled}
onValueChange={handleSelectChange}
onOpenChange={changeOpen}
onClose={onClose}
trigger={{
bound: true,
className: 'grow',
}}
overlayVisible={false}
>
{generateOptions()}
</Select>
<Input
value={level ? level : 1}
className={inputClasses}
fieldsetClassName={classNames({
hidden:
currentAwakening === undefined ||
currentAwakening.slug === defaultAwakening.slug,
})}
wrapperClassName="fullHeight"
bound={true}
type="number"
placeholder={rangeString()}
min={1}
max={maxLevel}
step="1"
onChange={handleInputChange}
ref={inputRef}
/>
</div>
<p className={errorClasses}>{error}</p>
</div>
)
}
AwakeningSelectWithInput.defaultProps = defaultProps
export default AwakeningSelectWithInput