## 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.
217 lines
5.5 KiB
TypeScript
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
|