Merge pull request #147 from jedmund/character-mods

Add character mods and context menus
This commit is contained in:
Justin Edmund 2023-01-22 21:33:12 -08:00 committed by GitHub
commit 17addbcba6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 2911 additions and 856 deletions

View file

@ -1,13 +1,14 @@
.Account.Dialog { .Account.DialogContent {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $unit * 2; gap: $unit-2x;
width: $unit * 64; width: $unit * 64;
form { .Fields {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $unit * 2; gap: $unit-2x;
padding: 0 $unit-4x;
} }
.DialogDescription { .DialogDescription {

View file

@ -2,14 +2,15 @@ import React, { useEffect, useState } from 'react'
import { getCookie, setCookie } from 'cookies-next' import { getCookie, setCookie } from 'cookies-next'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import { useTheme } from 'next-themes'
import { import {
Dialog, Dialog,
DialogClose, DialogClose,
DialogContent,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '~components/Dialog' } from '~components/Dialog'
import DialogContent from '~components/DialogContent'
import Button from '~components/Button' import Button from '~components/Button'
import SelectItem from '~components/SelectItem' import SelectItem from '~components/SelectItem'
import PictureSelectItem from '~components/PictureSelectItem' import PictureSelectItem from '~components/PictureSelectItem'
@ -23,7 +24,6 @@ import { pictureData } from '~utils/pictureData'
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from '~public/icons/Cross.svg'
import './index.scss' import './index.scss'
import { useTheme } from 'next-themes'
type StateVariables = { type StateVariables = {
[key: string]: boolean [key: string]: boolean
@ -285,7 +285,7 @@ const AccountModal = (props: Props) => {
</li> </li>
</DialogTrigger> </DialogTrigger>
<DialogContent <DialogContent
className="Account Dialog" className="Account"
onOpenAutoFocus={(event: Event) => {}} onOpenAutoFocus={(event: Event) => {}}
onEscapeKeyDown={onEscapeKeyDown} onEscapeKeyDown={onEscapeKeyDown}
> >
@ -304,14 +304,18 @@ const AccountModal = (props: Props) => {
</div> </div>
<form onSubmit={update}> <form onSubmit={update}>
<div className="Fields">
{pictureField()} {pictureField()}
{genderField()} {genderField()}
{languageField()} {languageField()}
{themeField()} {themeField()}
</div>
<div className="DialogFooter">
<Button <Button
contained={true} contained={true}
text={t('modals.settings.buttons.confirm')} text={t('modals.settings.buttons.confirm')}
/> />
</div>
</form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View file

@ -7,46 +7,31 @@
width: 100vw; width: 100vw;
top: 0; top: 0;
left: 0; left: 0;
z-index: 21; z-index: 31;
} }
.Alert { .Alert {
background: $grey-100; background: var(--dialog-bg);
border-radius: $unit; border-radius: $unit;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $unit; gap: $unit-2x;
min-width: $unit * 20; min-width: 20vw;
max-width: $unit * 40; max-width: 30vw;
padding: $unit * 4; padding: $unit * 4;
.description { .description {
font-size: $font-regular; font-size: $font-regular;
line-height: 1.26; line-height: 1.4;
strong {
font-weight: $bold;
}
} }
.buttons { .buttons {
display: flex;
align-self: flex-end; align-self: flex-end;
} gap: $unit;
.Button {
font-size: $font-regular;
padding: ($unit * 1.5) ($unit * 2);
margin-top: $unit * 2;
&.btn-disabled {
background: $grey-90;
color: $grey-70;
cursor: not-allowed;
}
&:not(.btn-disabled) {
background: $grey-90;
color: $grey-50;
&:hover {
background: $grey-80;
}
}
} }
} }

View file

@ -3,12 +3,13 @@ import * as AlertDialog from '@radix-ui/react-alert-dialog'
import './index.scss' import './index.scss'
import Button from '~components/Button' import Button from '~components/Button'
import Overlay from '~components/Overlay'
// Props // Props
interface Props { interface Props {
open: boolean open: boolean
title?: string title?: string
message: string message: string | React.ReactNode
primaryAction?: () => void primaryAction?: () => void
primaryActionText?: string primaryActionText?: string
cancelAction: () => void cancelAction: () => void
@ -29,13 +30,18 @@ const Alert = (props: Props) => {
<div className="buttons"> <div className="buttons">
<AlertDialog.Cancel asChild> <AlertDialog.Cancel asChild>
<Button <Button
contained={true}
onClick={props.cancelAction} onClick={props.cancelAction}
text={props.cancelActionText} text={props.cancelActionText}
/> />
</AlertDialog.Cancel> </AlertDialog.Cancel>
{props.primaryAction ? ( {props.primaryAction ? (
<AlertDialog.Action onClick={props.primaryAction}> <AlertDialog.Action asChild>
{props.primaryActionText} <Button
contained={true}
onClick={props.primaryAction}
text={props.primaryActionText}
/>
</AlertDialog.Action> </AlertDialog.Action>
) : ( ) : (
'' ''
@ -43,6 +49,7 @@ const Alert = (props: Props) => {
</div> </div>
</AlertDialog.Content> </AlertDialog.Content>
</div> </div>
<Overlay open={props.open} visible={true} />
</AlertDialog.Portal> </AlertDialog.Portal>
</AlertDialog.Root> </AlertDialog.Root>
) )

View file

@ -1,199 +1,95 @@
import React, { ForwardedRef, useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router' import cloneDeep from 'lodash.clonedeep'
import { useTranslation } from 'next-i18next'
import Input from '~components/LabelledInput' import SelectWithInput from '~components/SelectWithInput'
import Select from '~components/Select' import { weaponAwakening, characterAwakening } from '~data/awakening'
import SelectItem from '~components/SelectItem'
import classNames from 'classnames'
import { weaponAwakening, characterAwakening } from '~utils/awakening'
import type { Awakening } from '~utils/awakening'
import './index.scss' import './index.scss'
interface Props { interface Props {
object: 'character' | 'weapon' object: 'character' | 'weapon'
awakeningType?: number type?: number
awakeningLevel?: number level?: number
onOpenChange: (open: boolean) => void onOpenChange?: (open: boolean) => void
sendValidity: (isValid: boolean) => void sendValidity: (isValid: boolean) => void
sendValues: (type: number, level: number) => void sendValues: (type: number, level: number) => void
} }
const AwakeningSelect = (props: Props) => { const AwakeningSelect = (props: Props) => {
const router = useRouter() // Data states
const locale = const [awakeningType, setAwakeningType] = useState(
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' props.object === 'weapon' ? 0 : 1
const { t } = useTranslation('common') )
const [open, setOpen] = useState(false)
// Refs
const awakeningLevelInput = React.createRef<HTMLInputElement>()
// States
const [awakeningType, setAwakeningType] = useState(-1)
const [awakeningLevel, setAwakeningLevel] = useState(1) const [awakeningLevel, setAwakeningLevel] = useState(1)
const [maxValue, setMaxValue] = useState(1) // Data
const chooseDataset = () => {
let list: ItemSkill[] = []
const [error, setError] = useState('') switch (props.object) {
case 'character':
// Classes list = characterAwakening
const inputClasses = classNames({ break
Bound: true, case 'weapon':
Hidden: awakeningType === -1, // WARNING: Clonedeep is masking a deeper error
// which is running this method every time this component is rerendered
// causing multiple "No awakening" items to be added
const awakening = cloneDeep(weaponAwakening)
awakening.unshift({
id: 0,
name: {
en: 'No awakening',
ja: '覚醒なし',
},
slug: 'no-awakening',
minValue: 0,
maxValue: 0,
fractional: false,
}) })
list = awakening
break
}
const errorClasses = classNames({ return list
errors: true, }
visible: error !== '',
})
// Set max value based on object type
useEffect(() => {
if (props.object === 'character') setMaxValue(9)
else if (props.object === 'weapon') setMaxValue(15)
}, [props.object])
// Set default awakening and level based on object type // Set default awakening and level based on object type
useEffect(() => { useEffect(() => {
let defaultAwakening = 0 const defaultAwakening = props.object === 'weapon' ? 0 : 1
if (props.object === 'weapon') defaultAwakening = -1 const type = props.type != undefined ? props.type : defaultAwakening
setAwakeningType( setAwakeningType(type)
props.awakeningType != undefined ? props.awakeningType : defaultAwakening setAwakeningLevel(props.level ? props.level : 1)
) }, [props.object, props.type, props.level])
setAwakeningLevel(props.awakeningLevel ? props.awakeningLevel : 1)
}, [props.object, props.awakeningType, props.awakeningLevel])
// Send awakening type and level when changed
useEffect(() => {
props.sendValues(awakeningType, awakeningLevel)
}, [props.sendValues, awakeningType, awakeningLevel])
// Send validity of form when awakening level changes // Send validity of form when awakening level changes
useEffect(() => { useEffect(() => {
props.sendValidity(awakeningLevel > 0 && error === '') props.sendValidity(awakeningLevel > 0)
}, [props.sendValidity, awakeningLevel, error]) }, [props.sendValidity, awakeningLevel])
// Classes // Classes
function changeOpen() { function changeOpen(open: boolean) {
setOpen(!open) if (props.onOpenChange) props.onOpenChange(open)
props.onOpenChange(!open)
} }
function onClose() { function handleValueChange(type: number, level: number) {
props.onOpenChange(false) setAwakeningType(type)
} setAwakeningLevel(level)
props.sendValues(type, level)
function generateOptions(object: 'character' | 'weapon') {
let options: Awakening[] = []
if (object === 'character') options = characterAwakening
else if (object === 'weapon') options = weaponAwakening
else return
let optionElements: React.ReactNode[] = options.map((awakening, i) => {
return (
<SelectItem key={i} value={awakening.id}>
{awakening.name[locale]}
</SelectItem>
)
})
if (object === 'weapon') {
optionElements?.unshift(
<SelectItem key={-1} value={-1}>
{t('awakening.no_type')}
</SelectItem>
)
}
return optionElements
}
function handleSelectChange(rawValue: string) {
const value = parseInt(rawValue)
setAwakeningType(value)
}
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = parseFloat(event.target.value)
if (handleLevelError(value)) setAwakeningLevel(value)
}
function handleLevelError(value: number) {
let error = ''
if (value < 1) {
error = t('awakening.errors.value_too_low', {
minValue: 1,
})
} else if (value > maxValue) {
error = t('awakening.errors.value_too_high', {
maxValue: maxValue,
})
} else if (value % 1 != 0) {
error = t('awakening.errors.value_not_whole')
} else if (!value || value <= 0) {
error = t('awakening.errors.value_empty')
} else {
error = ''
}
setError(error)
return error.length === 0
}
const rangeString = (object: 'character' | 'weapon') => {
let minValue = 1
let maxValue = 1
if (object === 'weapon') {
minValue = 1
maxValue = 15
} else if (object === 'character') {
minValue = 1
maxValue = 9
} else return
return `${minValue}~${maxValue}`
} }
return ( return (
<div className="AwakeningSelect"> <div className="Awakening">
<div className="AwakeningSet"> <SelectWithInput
<div className="fields"> object={`${props.object}_awakening`}
<Select dataSet={chooseDataset()}
key="awakening_type" selectValue={awakeningType}
value={`${awakeningType}`} inputValue={awakeningLevel}
open={open} onOpenChange={changeOpen}
onValueChange={handleSelectChange} sendValidity={props.sendValidity}
onOpenChange={() => changeOpen()} sendValues={handleValueChange}
onClose={onClose}
triggerClass="modal"
>
{generateOptions(props.object)}
</Select>
<Input
value={awakeningLevel}
className={inputClasses}
type="number"
placeholder={rangeString(props.object)}
min={1}
max={maxValue}
step="1"
onChange={handleInputChange}
visible={awakeningType !== -1 ? true : false}
ref={awakeningLevelInput}
/> />
</div> </div>
<p className={errorClasses}>{error}</p>
</div>
</div>
) )
} }

View file

@ -7,7 +7,7 @@ import SelectItem from '~components/SelectItem'
import classNames from 'classnames' import classNames from 'classnames'
import { axData } from '~utils/axData' import ax from '~data/ax'
import './index.scss' import './index.scss'
@ -155,7 +155,7 @@ const AXSelect = (props: Props) => {
if (props.currentSkills[0].modifier > -1 && primaryAxValueInput.current) { if (props.currentSkills[0].modifier > -1 && primaryAxValueInput.current) {
const modifier = props.currentSkills[0].modifier const modifier = props.currentSkills[0].modifier
const axSkill = axData[props.axType - 1][modifier] const axSkill = ax[props.axType - 1][modifier]
setupInput(axSkill, primaryAxValueInput.current) setupInput(axSkill, primaryAxValueInput.current)
} }
} }
@ -169,7 +169,7 @@ const AXSelect = (props: Props) => {
props.currentSkills[1].modifier != null props.currentSkills[1].modifier != null
) { ) {
const firstSkill = props.currentSkills[0] const firstSkill = props.currentSkills[0]
const primaryAxSkill = axData[props.axType - 1][firstSkill.modifier] const primaryAxSkill = ax[props.axType - 1][firstSkill.modifier]
const secondaryAxSkill = findSecondaryAxSkill( const secondaryAxSkill = findSecondaryAxSkill(
primaryAxSkill, primaryAxSkill,
props.currentSkills[1] props.currentSkills[1]
@ -185,7 +185,7 @@ const AXSelect = (props: Props) => {
} }
function findSecondaryAxSkill( function findSecondaryAxSkill(
axSkill: AxSkill | undefined, axSkill: ItemSkill | undefined,
skillAtIndex: SimpleAxSkill skillAtIndex: SimpleAxSkill
) { ) {
if (axSkill) if (axSkill)
@ -213,7 +213,7 @@ const AXSelect = (props: Props) => {
} }
function generateOptions(modifierSet: number) { function generateOptions(modifierSet: number) {
const axOptions = axData[props.axType - 1] const axOptions = ax[props.axType - 1]
let axOptionElements: React.ReactNode[] = [] let axOptionElements: React.ReactNode[] = []
if (modifierSet == 0) { if (modifierSet == 0) {
@ -264,7 +264,7 @@ const AXSelect = (props: Props) => {
secondaryAxModifierSelect.current && secondaryAxModifierSelect.current &&
secondaryAxValueInput.current secondaryAxValueInput.current
) { ) {
setupInput(axData[props.axType - 1][value], primaryAxValueInput.current) setupInput(ax[props.axType - 1][value], primaryAxValueInput.current)
setPrimaryAxValue(0) setPrimaryAxValue(0)
primaryAxValueInput.current.value = '' primaryAxValueInput.current.value = ''
@ -280,7 +280,7 @@ const AXSelect = (props: Props) => {
const value = parseInt(rawValue) const value = parseInt(rawValue)
setSecondaryAxModifier(value) setSecondaryAxModifier(value)
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier] const primaryAxSkill = ax[props.axType - 1][primaryAxModifier]
const currentAxSkill = primaryAxSkill.secondary const currentAxSkill = primaryAxSkill.secondary
? primaryAxSkill.secondary.find((skill) => skill.id == value) ? primaryAxSkill.secondary.find((skill) => skill.id == value)
: undefined : undefined
@ -304,7 +304,7 @@ const AXSelect = (props: Props) => {
} }
function handlePrimaryErrors(value: number) { function handlePrimaryErrors(value: number) {
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier] const primaryAxSkill = ax[props.axType - 1][primaryAxModifier]
let newErrors = { ...errors } let newErrors = { ...errors }
if (value < primaryAxSkill.minValue) { if (value < primaryAxSkill.minValue) {
@ -333,7 +333,7 @@ const AXSelect = (props: Props) => {
} }
function handleSecondaryErrors(value: number) { function handleSecondaryErrors(value: number) {
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier] const primaryAxSkill = ax[props.axType - 1][primaryAxModifier]
let newErrors = { ...errors } let newErrors = { ...errors }
if (primaryAxSkill.secondary) { if (primaryAxSkill.secondary) {
@ -373,7 +373,7 @@ const AXSelect = (props: Props) => {
return newErrors.axValue2.length === 0 return newErrors.axValue2.length === 0
} }
function setupInput(ax: AxSkill | undefined, element: HTMLInputElement) { function setupInput(ax: ItemSkill | undefined, element: HTMLInputElement) {
if (ax) { if (ax) {
const rangeString = `${ax.minValue}~${ax.maxValue}${ax.suffix || ''}` const rangeString = `${ax.minValue}~${ax.maxValue}${ax.suffix || ''}`

View file

@ -8,6 +8,7 @@
font-size: $font-button; font-size: $font-button;
font-weight: $normal; font-weight: $normal;
gap: 6px; gap: 6px;
transition: 0.18s opacity ease-in-out;
&:hover, &:hover,
&.Blended:hover, &.Blended:hover,
@ -61,6 +62,14 @@
} }
} }
&.Options {
box-shadow: 0px 1px 3px rgb(0 0 0 / 14%);
position: absolute;
left: 8px;
top: 8px;
z-index: 3;
}
&:disabled { &:disabled {
background-color: var(--button-bg-disabled); background-color: var(--button-bg-disabled);
color: var(--button-text-disabled); color: var(--button-text-disabled);

View file

@ -2,7 +2,8 @@ import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { Trans, useTranslation } from 'next-i18next' import { Trans, useTranslation } from 'next-i18next'
import { Dialog, DialogContent } from '~components/Dialog' import { Dialog } from '~components/Dialog'
import DialogContent from '~components/DialogContent'
import Button from '~components/Button' import Button from '~components/Button'
import Overlay from '~components/Overlay' import Overlay from '~components/Overlay'
@ -71,7 +72,7 @@ const CharacterConflictModal = (props: Props) => {
return ( return (
<Dialog open={open} onOpenChange={openChange}> <Dialog open={open} onOpenChange={openChange}>
<DialogContent <DialogContent
className="Conflict Dialog" className="Conflict"
onOpenAutoFocus={(event) => event.preventDefault()} onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={close} onEscapeKeyDown={close}
> >

View file

@ -171,6 +171,15 @@ const CharacterGrid = (props: Props) => {
setIncoming(undefined) setIncoming(undefined)
} }
async function removeCharacter(id: string) {
try {
const response = await api.endpoints.grid_characters.destroy({ id: id })
appState.grid.characters[response.data.position] = undefined
} catch (error) {
console.error(error)
}
}
// Methods: Saving job and job skills // Methods: Saving job and job skills
const saveJob = async function (job?: Job) { const saveJob = async function (job?: Job) {
const payload = { const payload = {
@ -371,6 +380,7 @@ const CharacterGrid = (props: Props) => {
position={i} position={i}
updateObject={receiveCharacterFromSearch} updateObject={receiveCharacterFromSearch}
updateUncap={initiateUncapUpdate} updateUncap={initiateUncapUpdate}
removeCharacter={removeCharacter}
/> />
</li> </li>
) )

View file

@ -0,0 +1,78 @@
.Character.DialogContent {
gap: $unit;
min-width: 480px;
@include breakpoint(phone) {
min-width: inherit;
}
.DialogHeader {
transition: 0.18s padding-top ease-in-out;
position: sticky;
top: 0;
&.Scrolled {
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 12px rgba(0, 0, 0, 0.34);
padding-top: $unit-2x;
}
img {
transition: 0.2s width ease-in-out;
width: $unit-6x !important;
}
.DialogTitle {
font-size: $font-large;
}
.SubTitle {
display: none;
}
}
.mods {
display: flex;
flex-direction: column;
gap: $unit-4x;
padding: 0 $unit-4x;
section {
display: flex;
flex-direction: column;
gap: $unit-half;
&.inline {
align-items: center;
flex-direction: row;
justify-content: space-between;
h3 {
margin: 0;
}
}
h3 {
color: $grey-55;
font-size: $font-small;
margin-bottom: $unit;
}
select {
background-color: $grey-90;
}
}
.Button {
font-size: $font-regular;
padding: ($unit * 1.5) ($unit-2x);
width: 100%;
&.btn-disabled {
background: $grey-90;
color: $grey-70;
cursor: not-allowed;
}
}
}
}

View file

@ -0,0 +1,327 @@
// Core dependencies
import React, {
PropsWithChildren,
useCallback,
useEffect,
useState,
} from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import { AxiosResponse } from 'axios'
import classNames from 'classnames'
// UI dependencies
import {
Dialog,
DialogClose,
DialogTitle,
DialogTrigger,
} from '~components/Dialog'
import DialogContent from '~components/DialogContent'
import Button from '~components/Button'
import SelectWithInput from '~components/SelectWithInput'
import AwakeningSelect from '~components/AwakeningSelect'
import RingSelect from '~components/RingSelect'
import Switch from '~components/Switch'
// Utilities
import api from '~utils/api'
import { appState } from '~utils/appState'
import { retrieveCookies } from '~utils/retrieveCookies'
import elementalizeAetherialMastery from '~utils/elementalizeAetherialMastery'
// Data
const emptyExtendedMastery: ExtendedMastery = {
modifier: 0,
strength: 0,
}
// Styles and icons
import CrossIcon from '~public/icons/Cross.svg'
import './index.scss'
// Types
import { CharacterOverMastery, ExtendedMastery } from '~types'
interface GridCharacterObject {
character: {
ring1: ExtendedMastery
ring2: ExtendedMastery
ring3: ExtendedMastery
ring4: ExtendedMastery
earring: ExtendedMastery
awakening: {
type?: number
level?: number
}
transcendence_step: number
perpetuity: boolean
}
}
interface Props {
gridCharacter: GridCharacter
open: boolean
onOpenChange: (open: boolean) => void
}
const CharacterModal = ({
gridCharacter,
children,
open: modalOpen,
onOpenChange,
}: PropsWithChildren<Props>) => {
const router = useRouter()
const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const { t } = useTranslation('common')
// Cookies
const cookies = retrieveCookies()
// UI state
const [open, setOpen] = useState(false)
const [formValid, setFormValid] = useState(false)
// Classes
const headerClasses = classNames({
DialogHeader: true,
Scrolled: scrolled,
})
useEffect(() => {
setOpen(modalOpen)
}, [modalOpen])
// Character properties: Perpetuity
const [perpetuity, setPerpetuity] = useState(false)
// Character properties: Ring
const [rings, setRings] = useState<CharacterOverMastery>({
1: { ...emptyExtendedMastery, modifier: 1 },
2: { ...emptyExtendedMastery, modifier: 2 },
3: emptyExtendedMastery,
4: emptyExtendedMastery,
})
// Character properties: Earrings
const [earring, setEarring] = useState<ExtendedMastery>(emptyExtendedMastery)
// Character properties: Awakening
const [awakeningType, setAwakeningType] = useState(0)
const [awakeningLevel, setAwakeningLevel] = useState(0)
// Character properties: Transcendence
const [transcendenceStep, setTranscendenceStep] = useState(0)
// Hooks
useEffect(() => {
if (gridCharacter.aetherial_mastery) {
setEarring({
modifier: gridCharacter.aetherial_mastery.modifier,
strength: gridCharacter.aetherial_mastery.strength,
})
}
setAwakeningType(gridCharacter.awakening.type)
setAwakeningLevel(gridCharacter.awakening.level)
setPerpetuity(gridCharacter.perpetuity)
}, [gridCharacter])
// Methods: UI state management
function handleOpenChange(open: boolean) {
setOpen(open)
onOpenChange(open)
}
// Methods: Receive data from components
function receiveRingValues(overMastery: CharacterOverMastery) {
setRings(overMastery)
}
function receiveEarringValues(
earringModifier: number,
earringStrength: number
) {
setEarring({
modifier: earringModifier,
strength: earringStrength,
})
}
function handleCheckedChange(checked: boolean) {
setPerpetuity(checked)
}
function receiveAwakeningValues(type: number, level: number) {
setAwakeningType(type)
setAwakeningLevel(level)
}
function receiveValidity(isValid: boolean) {
setFormValid(isValid)
}
// Methods: Data syncing
// Prepare the GridWeaponObject to send to the server
function prepareObject() {
let object: GridCharacterObject = {
character: {
ring1: {
modifier: rings[1].modifier,
strength: rings[1].strength,
},
ring2: {
modifier: rings[2].modifier,
strength: rings[2].strength,
},
ring3: {
modifier: rings[3].modifier,
strength: rings[3].strength,
},
ring4: {
modifier: rings[4].modifier,
strength: rings[4].strength,
},
earring: {
modifier: earring.modifier,
strength: earring.strength,
},
awakening: {
type: awakeningType,
level: awakeningLevel,
},
transcendence_step: transcendenceStep,
perpetuity: perpetuity,
},
}
return object
}
// Send the GridWeaponObject to the server
async function updateCharacter() {
const updateObject = prepareObject()
return await api.endpoints.grid_characters
.update(gridCharacter.id, updateObject)
.then((response) => processResult(response))
.catch((error) => processError(error))
}
// Save the server's response to state
function processResult(response: AxiosResponse) {
const gridCharacter: GridCharacter = response.data
appState.grid.characters[gridCharacter.position] = gridCharacter
setOpen(false)
if (onOpenChange) onOpenChange(false)
}
function processError(error: any) {
console.error(error)
}
const ringSelect = () => {
return (
<section>
<h3>{t('modals.characters.subtitles.ring')}</h3>
<RingSelect
gridCharacter={gridCharacter}
sendValues={receiveRingValues}
/>
</section>
)
}
const earringSelect = () => {
const earringData = elementalizeAetherialMastery(gridCharacter)
return (
<section>
<h3>{t('modals.characters.subtitles.earring')}</h3>
<SelectWithInput
object="earring"
dataSet={earringData}
selectValue={earring.modifier ? earring.modifier : 0}
inputValue={earring.strength ? earring.strength : 0}
sendValidity={receiveValidity}
sendValues={receiveEarringValues}
/>
</section>
)
}
const awakeningSelect = () => {
return (
<section>
<h3>{t('modals.characters.subtitles.awakening')}</h3>
<AwakeningSelect
object="character"
type={awakeningType}
level={awakeningLevel}
sendValidity={receiveValidity}
sendValues={receiveAwakeningValues}
/>
</section>
)
}
const perpetuitySwitch = () => {
return (
<section className="inline">
<h3>{t('modals.characters.subtitles.permanent')}</h3>
<Switch onCheckedChange={handleCheckedChange} checked={perpetuity} />
</section>
)
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent
className="Character"
onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={() => {}}
>
<div className={headerClasses}>
<img
alt={gridCharacter.object.name[locale]}
className="DialogImage"
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-square/${gridCharacter.object.granblue_id}_01.jpg`}
/>
<div className="DialogTop">
<DialogTitle className="SubTitle">
{t('modals.characters.title')}
</DialogTitle>
<DialogTitle className="DialogTitle">
{gridCharacter.object.name[locale]}
</DialogTitle>
</div>
<DialogClose className="DialogClose" asChild>
<span>
<CrossIcon />
</span>
</DialogClose>
</div>
<div className="mods">
{perpetuitySwitch()}
{ringSelect()}
{earringSelect()}
{awakeningSelect()}
</div>
<div className="DialogFooter">
<Button
contained={true}
onClick={updateCharacter}
disabled={!formValid}
text={t('modals.characters.buttons.confirm')}
/>
</div>
</DialogContent>
</Dialog>
)
}
export default CharacterModal

View file

@ -5,6 +5,7 @@
gap: calc($unit / 2); gap: calc($unit / 2);
// min-height: 320px; // min-height: 320px;
// max-width: 200px; // max-width: 200px;
position: relative;
margin-bottom: $unit * 4; margin-bottom: $unit * 4;
&.editable .CharacterImage:hover { &.editable .CharacterImage:hover {
@ -22,6 +23,17 @@
display: flex; display: flex;
} }
.Button {
pointer-events: none;
opacity: 0;
}
&:hover .Button,
.Button.Clicked {
pointer-events: initial;
opacity: 1;
}
h3, h3,
ul { ul {
display: none; display: none;
@ -57,9 +69,11 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: hidden; overflow: hidden;
transition: all 0.18s ease-in-out; transition: $duration-zoom all ease-in-out;
height: auto; height: auto;
width: 100%; width: 100%;
-webkit-user-select: none; /* Safari */
user-select: none;
&:hover .icon svg { &:hover .icon svg {
fill: var(--icon-secondary-hover); fill: var(--icon-secondary-hover);
@ -72,6 +86,7 @@
z-index: 1; z-index: 1;
svg { svg {
transition: $duration-color-fade fill ease-in-out;
fill: var(--icon-secondary); fill: var(--icon-secondary);
} }
} }

View file

@ -1,15 +1,26 @@
import React, { useEffect, useState } from 'react' import React, { MouseEvent, useEffect, useState } from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next' import { Trans, useTranslation } from 'next-i18next'
import classnames from 'classnames' import classNames from 'classnames'
import Alert from '~components/Alert'
import Button from '~components/Button'
import CharacterHovercard from '~components/CharacterHovercard'
import CharacterModal from '~components/CharacterModal'
import {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
} from '~components/ContextMenu'
import ContextMenuItem from '~components/ContextMenuItem'
import SearchModal from '~components/SearchModal'
import UncapIndicator from '~components/UncapIndicator'
import { appState } from '~utils/appState' import { appState } from '~utils/appState'
import CharacterHovercard from '~components/CharacterHovercard'
import SearchModal from '~components/SearchModal'
import UncapIndicator from '~components/UncapIndicator'
import PlusIcon from '~public/icons/Add.svg' import PlusIcon from '~public/icons/Add.svg'
import SettingsIcon from '~public/icons/Settings.svg'
import type { SearchableObject } from '~types' import type { SearchableObject } from '~types'
@ -19,48 +30,112 @@ interface Props {
gridCharacter?: GridCharacter gridCharacter?: GridCharacter
position: number position: number
editable: boolean editable: boolean
removeCharacter: (id: string) => void
updateObject: (object: SearchableObject, position: number) => void updateObject: (object: SearchableObject, position: number) => void
updateUncap: (id: string, position: number, uncap: number) => void updateUncap: (id: string, position: number, uncap: number) => void
} }
const CharacterUnit = (props: Props) => { const CharacterUnit = ({
gridCharacter,
position,
editable,
removeCharacter: sendCharacterToRemove,
updateObject,
updateUncap,
}: Props) => {
// Translations and locale
const { t } = useTranslation('common') const { t } = useTranslation('common')
const { party, grid } = useSnapshot(appState)
const router = useRouter() const router = useRouter()
const locale = const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
// State snapshot
const { party, grid } = useSnapshot(appState)
// State: UI
const [detailsModalOpen, setDetailsModalOpen] = useState(false)
const [searchModalOpen, setSearchModalOpen] = useState(false)
const [contextMenuOpen, setContextMenuOpen] = useState(false)
const [alertOpen, setAlertOpen] = useState(false)
// State: Other
const [imageUrl, setImageUrl] = useState('') const [imageUrl, setImageUrl] = useState('')
const classes = classnames({ // Classes
const classes = classNames({
CharacterUnit: true, CharacterUnit: true,
editable: props.editable, editable: editable,
filled: props.gridCharacter !== undefined, filled: gridCharacter !== undefined,
}) })
const gridCharacter = props.gridCharacter const buttonClasses = classNames({
Options: true,
Clicked: contextMenuOpen,
})
// Other
const character = gridCharacter?.object const character = gridCharacter?.object
// Hooks
useEffect(() => { useEffect(() => {
generateImageUrl() generateImageUrl()
}) })
// Methods: Open layer
function openCharacterModal(event: Event) {
setDetailsModalOpen(true)
}
function openSearchModal(event: MouseEvent<HTMLDivElement>) {
if (editable) setSearchModalOpen(true)
}
function openRemoveCharacterAlert() {
setAlertOpen(true)
}
// Methods: Handle button clicked
function handleButtonClicked() {
setContextMenuOpen(!contextMenuOpen)
}
// Methods: Handle open change
function handleCharacterModalOpenChange(open: boolean) {
setDetailsModalOpen(open)
}
function handleSearchModalOpenChange(open: boolean) {
setSearchModalOpen(open)
}
function handleContextMenuOpenChange(open: boolean) {
if (!open) setContextMenuOpen(false)
}
// Methods: Mutate data
function passUncapData(uncap: number) {
if (gridCharacter) updateUncap(gridCharacter.id, position, uncap)
}
function removeCharacter() {
if (gridCharacter) sendCharacterToRemove(gridCharacter.id)
}
// Methods: Image string generation
function generateImageUrl() { function generateImageUrl() {
let imgSrc = '' let imgSrc = ''
if (props.gridCharacter) { if (gridCharacter) {
const character = props.gridCharacter.object! const character = gridCharacter.object!
// Change the image based on the uncap level // Change the image based on the uncap level
let suffix = '01' let suffix = '01'
if (props.gridCharacter.uncap_level == 6) suffix = '04' if (gridCharacter.uncap_level == 6) suffix = '04'
else if (props.gridCharacter.uncap_level == 5) suffix = '03' else if (gridCharacter.uncap_level == 5) suffix = '03'
else if (props.gridCharacter.uncap_level > 2) suffix = '02' else if (gridCharacter.uncap_level > 2) suffix = '02'
// Special casing for Lyria (and Young Cat eventually) // Special casing for Lyria (and Young Cat eventually)
if (props.gridCharacter.object.granblue_id === '3030182000') { if (gridCharacter.object.granblue_id === '3030182000') {
let element = 1 let element = 1
if (grid.weapons.mainWeapon && grid.weapons.mainWeapon.element) { if (grid.weapons.mainWeapon && grid.weapons.mainWeapon.element) {
element = grid.weapons.mainWeapon.element element = grid.weapons.mainWeapon.element
@ -77,15 +152,86 @@ const CharacterUnit = (props: Props) => {
setImageUrl(imgSrc) setImageUrl(imgSrc)
} }
function passUncapData(uncap: number) { // Methods: Layer element rendering
if (props.gridCharacter) const characterModal = () => {
props.updateUncap(props.gridCharacter.id, props.position, uncap) if (gridCharacter) {
return (
<CharacterModal
gridCharacter={gridCharacter}
open={detailsModalOpen}
onOpenChange={handleCharacterModalOpenChange}
/>
)
}
} }
const contextMenu = () => {
if (editable && gridCharacter && gridCharacter.id) {
return (
<>
<ContextMenu onOpenChange={handleContextMenuOpenChange}>
<ContextMenuTrigger asChild>
<Button
accessoryIcon={<SettingsIcon />}
className={buttonClasses}
onClick={handleButtonClicked}
/>
</ContextMenuTrigger>
<ContextMenuContent align="start">
<ContextMenuItem onSelect={openCharacterModal}>
{t('context.modify.character')}
</ContextMenuItem>
<ContextMenuItem onSelect={openRemoveCharacterAlert}>
{t('context.remove')}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
{characterModal()}
{removeAlert()}
</>
)
}
}
const removeAlert = () => {
return (
<Alert
open={alertOpen}
primaryAction={removeCharacter}
primaryActionText={t('modals.characters.buttons.remove')}
cancelAction={() => setAlertOpen(false)}
cancelActionText={t('buttons.cancel')}
message={
<Trans i18nKey="modals.characters.messages.remove">
Are you sure you want to remove{' '}
<strong>{{ character: gridCharacter?.object.name[locale] }}</strong>{' '}
from your team?
</Trans>
}
/>
)
}
const searchModal = () => {
if (editable) {
return (
<SearchModal
placeholderText={t('search.placeholders.character')}
fromPosition={position}
object="characters"
open={searchModalOpen}
onOpenChange={handleSearchModalOpenChange}
send={updateObject}
/>
)
}
}
// Methods: Core element rendering
const image = ( const image = (
<div className="CharacterImage"> <div className="CharacterImage" onClick={openSearchModal}>
<img alt={character?.name.en} className="grid_image" src={imageUrl} /> <img alt={character?.name.en} className="grid_image" src={imageUrl} />
{props.editable ? ( {editable ? (
<span className="icon"> <span className="icon">
<PlusIcon /> <PlusIcon />
</span> </span>
@ -95,20 +241,11 @@ const CharacterUnit = (props: Props) => {
</div> </div>
) )
const editableImage = (
<SearchModal
placeholderText={t('search.placeholders.character')}
fromPosition={props.position}
object="characters"
send={props.updateObject}
>
{image}
</SearchModal>
)
const unitContent = ( const unitContent = (
<>
<div className={classes}> <div className={classes}>
{props.editable ? editableImage : image} {contextMenu()}
{image}
{gridCharacter && character ? ( {gridCharacter && character ? (
<UncapIndicator <UncapIndicator
type="character" type="character"
@ -123,15 +260,17 @@ const CharacterUnit = (props: Props) => {
)} )}
<h3 className="CharacterName">{character?.name[locale]}</h3> <h3 className="CharacterName">{character?.name[locale]}</h3>
</div> </div>
{searchModal()}
</>
) )
const withHovercard = ( const unitContentWithHovercard = (
<CharacterHovercard gridCharacter={gridCharacter!}> <CharacterHovercard gridCharacter={gridCharacter!}>
{unitContent} {unitContent}
</CharacterHovercard> </CharacterHovercard>
) )
return gridCharacter && !props.editable ? withHovercard : unitContent return gridCharacter && !editable ? unitContentWithHovercard : unitContent
} }
export default CharacterUnit export default CharacterUnit

View file

@ -0,0 +1,6 @@
.ContextMenu {
background: var(--menu-bg);
border-radius: $input-corner;
padding: $unit 0;
margin-top: $unit-fourth;
}

View file

@ -0,0 +1,36 @@
import React from 'react'
import classNames from 'classnames'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import './index.scss'
interface Props
extends React.DetailedHTMLProps<
React.DialogHTMLAttributes<HTMLDivElement>,
HTMLDivElement
> {
align?: 'start' | 'center' | 'end'
}
export const ContextMenuContent = React.forwardRef<HTMLDivElement, Props>(
function ContextMenu({ children, ...props }, forwardedRef) {
const classes = classNames(
{
ContextMenu: true,
},
props.className
)
return (
<DropdownMenu.Portal>
<DropdownMenu.Content className={classes} {...props} ref={forwardedRef}>
{children}
</DropdownMenu.Content>
</DropdownMenu.Portal>
)
}
)
export const ContextMenu = DropdownMenu.Root
export const ContextMenuGroup = DropdownMenu.Group
export const ContextMenuTrigger = DropdownMenu.Trigger

View file

@ -0,0 +1,11 @@
.ContextItem {
color: var(--menu-text);
font-size: $font-regular;
padding: ($unit * 1.5) $unit-2x;
&:hover {
background: var(--menu-bg-item-hover);
color: var(--text-primary);
cursor: pointer;
}
}

View file

@ -0,0 +1,30 @@
import React from 'react'
import classNames from 'classnames'
import { DropdownMenuItem } from '@radix-ui/react-dropdown-menu'
import './index.scss'
interface Props {
className?: string
onSelect?: (event: Event) => void
children: React.ReactNode
}
const ContextMenuItem = React.forwardRef<HTMLDivElement, Props>(
function ContextMenu({ children, ...props }, forwardedRef) {
const classes = classNames(
{
ContextItem: true,
},
props.className
)
return (
<DropdownMenuItem className={classes} onSelect={props.onSelect}>
{children}
</DropdownMenuItem>
)
}
)
export default ContextMenuItem

View file

@ -1,211 +0,0 @@
.Dialog {
$multiplier: 4;
animation: 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal none running
openModalDesktop;
background: var(--dialog-bg);
border-radius: $card-corner;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: $unit * $multiplier;
height: auto;
min-width: $unit * 48;
min-height: $unit-12x;
min-width: 580px;
padding: $unit * $multiplier;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 40;
a:hover {
text-decoration: underline;
}
@include breakpoint(phone) {
animation: 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal forwards running
openModalMobile;
min-width: inherit;
min-height: 80vh;
transform: initial;
left: 0;
right: 0;
top: 0;
height: auto;
width: 100%;
}
.DialogHeader {
display: flex;
align-items: center;
gap: $unit;
justify-content: space-between;
.left {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: $unit;
p {
font-size: $font-small;
line-height: 1.25;
}
}
}
.DialogClose {
background: transparent;
border: none;
&:hover {
cursor: pointer;
svg {
fill: $error;
}
}
svg {
fill: $grey-50;
float: right;
height: 24px;
width: 24px;
}
}
.DialogTitle {
color: var(--text-primary);
font-size: $font-xlarge;
h1 {
color: var(--text-primary);
font-size: $font-xlarge;
font-weight: $medium;
text-align: left;
}
}
.DialogTop {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: calc($unit / 2);
.SubTitle {
color: var(--text-secondary);
font-size: $font-small;
font-weight: $medium;
}
}
.DialogDescription {
color: var(--text-secondary);
flex-grow: 1;
}
.actions {
display: flex;
justify-content: flex-end;
width: 100%;
}
&.Conflict.Dialog {
$weapon-diameter: 14rem;
& > p {
line-height: 1.2;
max-width: 400px;
strong {
font-weight: $bold;
}
&:lang(ja) {
line-height: 1.4;
}
}
.weapon,
.character {
display: flex;
flex-direction: column;
gap: $unit;
text-align: center;
width: $weapon-diameter;
font-weight: $medium;
img {
border-radius: 1rem;
width: $weapon-diameter;
height: auto;
}
span {
line-height: 1.3;
}
}
.Diagram {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: flex-start;
&.CharacterDiagram {
align-items: center;
}
ul {
align-items: center;
display: flex;
flex-direction: column;
gap: $unit * 2;
}
.wrapper {
display: flex;
justify-content: center;
width: 100%;
}
.arrow {
align-items: center;
color: $grey-55;
display: flex;
font-size: 4rem;
text-align: center;
height: $weapon-diameter;
justify-content: center;
}
}
footer {
display: flex;
flex-direction: row;
gap: $unit;
.Button {
font-size: $font-regular;
padding: ($unit * 1.5) ($unit * 2);
width: 100%;
&.btn-disabled {
background: $grey-90;
color: $grey-70;
cursor: not-allowed;
}
&:not(.btn-disabled) {
background: $grey-90;
color: $grey-50;
&:hover {
background: $grey-80;
}
}
}
}
}
}

View file

@ -1,46 +1,37 @@
import React from 'react' import React, { PropsWithChildren, useEffect, useState } from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog' import * as DialogPrimitive from '@radix-ui/react-dialog'
import classNames from 'classnames' import { useLockedBody } from 'usehooks-ts'
import './index.scss' import './index.scss'
import Overlay from '~components/Overlay'
interface Props interface Props extends DialogPrimitive.DialogProps {}
extends React.DetailedHTMLProps<
React.DialogHTMLAttributes<HTMLDivElement>, export const Dialog = ({ children, ...props }: PropsWithChildren<Props>) => {
HTMLDivElement const [locked, setLocked] = useLockedBody(false, 'root')
> { const [open, setOpen] = useState(false)
onEscapeKeyDown: (event: KeyboardEvent) => void
onOpenAutoFocus: (event: Event) => void useEffect(() => {
if (props.open != undefined) {
toggleLocked(props.open)
setOpen(props.open)
}
}, [props.open])
function toggleLocked(open: boolean) {
setLocked(open)
} }
export const DialogContent = React.forwardRef<HTMLDivElement, Props>( function handleOpenChange(open: boolean) {
function dialog({ children, ...props }, forwardedRef) { if (props.onOpenChange) props.onOpenChange(open)
const classes = classNames( }
{
Dialog: true,
},
props.className
)
return ( return (
<DialogPrimitive.Portal> <DialogPrimitive.Root open={props.open} onOpenChange={handleOpenChange}>
<DialogPrimitive.Content
className={classes}
{...props}
onOpenAutoFocus={props.onOpenAutoFocus}
onEscapeKeyDown={props.onEscapeKeyDown}
ref={forwardedRef}
>
{children} {children}
</DialogPrimitive.Content> </DialogPrimitive.Root>
<Overlay visible={true} open={true} />
</DialogPrimitive.Portal>
) )
} }
)
export const Dialog = DialogPrimitive.Root
export const DialogTitle = DialogPrimitive.Title export const DialogTitle = DialogPrimitive.Title
export const DialogTrigger = DialogPrimitive.Trigger export const DialogTrigger = DialogPrimitive.Trigger
export const DialogClose = DialogPrimitive.Close export const DialogClose = DialogPrimitive.Close

View file

@ -0,0 +1,243 @@
.Dialog {
// animation: 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal none running
// openModalDesktop;
position: fixed;
background: none;
border: 0;
inset: 0;
display: grid;
place-items: center;
min-height: 100vh;
min-width: 100vw;
overflow-y: auto;
color: inherit;
z-index: 40;
.DialogContent {
$multiplier: 4;
background: var(--dialog-bg);
border-radius: $card-corner;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: $unit * $multiplier;
height: auto;
min-width: $unit * 48;
// min-height: $unit-12x;
overflow-y: scroll;
max-height: 80vh;
min-width: 580px;
// padding: $unit * $multiplier;
position: relative;
a:hover {
text-decoration: underline;
}
@include breakpoint(phone) {
animation: 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal forwards running
openModalMobile;
min-width: inherit;
min-height: 80vh;
transform: initial;
left: 0;
right: 0;
top: 0;
height: auto;
width: 100%;
}
.DialogHeader {
background: var(--dialog-bg);
display: flex;
align-items: center;
gap: $unit-2x;
justify-content: space-between;
padding: $unit-3x ($unit * $multiplier);
position: sticky;
top: 0;
z-index: 10;
.left {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: $unit;
p {
font-size: $font-small;
line-height: 1.25;
}
}
.DialogImage {
border-radius: $input-corner;
width: $unit-10x;
}
}
.DialogClose {
background: transparent;
border: none;
&:hover {
cursor: pointer;
svg {
fill: $error;
}
}
svg {
fill: $grey-50;
float: right;
height: 24px;
width: 24px;
}
}
.DialogTitle {
color: var(--text-primary);
font-size: $font-xlarge;
h1 {
color: var(--text-primary);
font-size: $font-xlarge;
font-weight: $medium;
text-align: left;
}
}
.DialogTop {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: calc($unit / 2);
.SubTitle {
color: var(--text-secondary);
font-size: $font-small;
font-weight: $medium;
}
}
.DialogDescription {
color: var(--text-secondary);
flex-grow: 1;
}
.DialogFooter {
align-items: flex-end;
background: var(--dialog-bg);
bottom: 0;
display: flex;
flex-direction: column;
padding: ($unit * 1.5) ($unit * $multiplier) $unit-3x;
position: sticky;
}
.actions {
display: flex;
justify-content: flex-end;
width: 100%;
}
&.Conflict {
$weapon-diameter: 14rem;
& > p {
line-height: 1.2;
max-width: 400px;
strong {
font-weight: $bold;
}
&:lang(ja) {
line-height: 1.4;
}
}
.weapon,
.character {
display: flex;
flex-direction: column;
gap: $unit;
text-align: center;
width: $weapon-diameter;
font-weight: $medium;
img {
border-radius: 1rem;
width: $weapon-diameter;
height: auto;
}
span {
line-height: 1.3;
}
}
.Diagram {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: flex-start;
&.CharacterDiagram {
align-items: center;
}
ul {
align-items: center;
display: flex;
flex-direction: column;
gap: $unit * 2;
}
.wrapper {
display: flex;
justify-content: center;
width: 100%;
}
.arrow {
align-items: center;
color: $grey-55;
display: flex;
font-size: 4rem;
text-align: center;
height: $weapon-diameter;
justify-content: center;
}
}
footer {
display: flex;
flex-direction: row;
gap: $unit;
.Button {
font-size: $font-regular;
padding: ($unit * 1.5) ($unit * 2);
width: 100%;
&.btn-disabled {
background: $grey-90;
color: $grey-70;
cursor: not-allowed;
}
&:not(.btn-disabled) {
background: $grey-90;
color: $grey-50;
&:hover {
background: $grey-80;
}
}
}
}
}
}
}

View file

@ -0,0 +1,43 @@
import React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import classNames from 'classnames'
import './index.scss'
import Overlay from '~components/Overlay'
interface Props
extends React.DetailedHTMLProps<
React.DialogHTMLAttributes<HTMLDivElement>,
HTMLDivElement
> {
onEscapeKeyDown: (event: KeyboardEvent) => void
onOpenAutoFocus: (event: Event) => void
}
const DialogContent = React.forwardRef<HTMLDivElement, Props>(function dialog(
{ children, ...props },
forwardedRef
) {
const classes = classNames(props.className, {
DialogContent: true,
})
return (
<DialogPrimitive.Portal>
<dialog className="Dialog">
<DialogPrimitive.Content
{...props}
className={classes}
onOpenAutoFocus={props.onOpenAutoFocus}
onEscapeKeyDown={props.onEscapeKeyDown}
ref={forwardedRef}
>
{children}
</DialogPrimitive.Content>
</dialog>
<Overlay visible={true} open={true} />
</DialogPrimitive.Portal>
)
})
export default DialogContent

View file

@ -0,0 +1,17 @@
.SelectSet {
display: flex;
flex-direction: row;
gap: $unit;
width: 100%;
.SelectTrigger.Left {
flex-grow: 1;
width: 100%;
}
.SelectTrigger.Right {
flex-grow: 0;
text-align: right;
min-width: 12rem;
}
}

View file

@ -0,0 +1,163 @@
// 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 Select from '~components/Select'
import SelectItem from '~components/SelectItem'
// Styles and icons
import './index.scss'
// Types
interface Props {
name: string
object: 'ring'
dataSet: ItemSkill[]
leftSelectValue: number
leftSelectDisabled: boolean
rightSelectValue: number
sendValues: (left: number, right: number) => void
}
const defaultProps = {
selectDisabled: false,
}
const ExtendedMasterySelect = ({
name,
object,
dataSet,
leftSelectDisabled,
leftSelectValue,
rightSelectValue,
sendValues,
}: Props) => {
const router = useRouter()
const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const { t } = useTranslation('common')
// UI state
const [leftSelectOpen, setLeftSelectOpen] = useState(false)
const [rightSelectOpen, setRightSelectOpen] = useState(false)
// Field properties
// prettier-ignore
const [currentItemSkill, setCurrentItemSkill] = useState<ItemSkill | undefined>(undefined)
const [currentItemValue, setCurrentItemValue] = useState(rightSelectValue)
// Hooks
// if (currentItemSkill) sendValues(currentItemSkill.id, currentItemValue)
// Set default values from props
useEffect(() => {
setCurrentItemSkill(dataSet.find((sk) => sk.id === leftSelectValue))
setCurrentItemValue(rightSelectValue)
}, [leftSelectValue, rightSelectValue])
// Methods: UI state management
function changeOpen(side: 'left' | 'right') {
if (side === 'left' && !leftSelectDisabled) {
setLeftSelectOpen(!leftSelectOpen)
} else if (side === 'right') {
setRightSelectOpen(!rightSelectOpen)
}
}
function onClose() {
setLeftSelectOpen(false)
setRightSelectOpen(false)
}
// Methods: Rendering
function generateLeftOptions() {
let options: React.ReactNode[] = dataSet.map((skill, i) => {
return (
<SelectItem key={`${name}-key-${i}`} value={skill.id}>
{skill.name[locale]}
</SelectItem>
)
})
return options
}
function generateRightOptions() {
if (currentItemSkill && currentItemSkill.values) {
let options = currentItemSkill.values.map((value, i) => {
return (
<SelectItem key={`${name}-values-${i + 1}`} value={value}>
{value}
{currentItemSkill.suffix ? currentItemSkill.suffix : ''}
</SelectItem>
)
})
options.unshift(
<SelectItem key={`${name}-values-0`} value="no-value">
{t('no_value')}
</SelectItem>
)
return options
}
}
// Methods: User input detection
function handleLeftSelectChange(rawValue: string) {
const value = parseInt(rawValue)
const skill = dataSet.find((sk) => sk.id === value)
setCurrentItemSkill(skill)
setCurrentItemValue(0)
if (skill) sendValues(skill.id, 0)
}
function handleRightSelectChange(rawValue: string) {
const value = parseFloat(rawValue)
setCurrentItemValue(value)
if (currentItemSkill) sendValues(currentItemSkill.id, value)
}
return (
<div className="SelectSet">
<Select
key={`${name}_type`}
value={`${currentItemSkill ? currentItemSkill.id : 0}`}
open={leftSelectOpen}
disabled={leftSelectDisabled}
onValueChange={handleLeftSelectChange}
onOpenChange={() => changeOpen('left')}
onClose={onClose}
triggerClass="Left modal"
>
{generateLeftOptions()}
</Select>
<Select
key={`${name}_value`}
value={`${currentItemValue > 0 ? currentItemValue : 'no-value'}`}
open={rightSelectOpen}
onValueChange={handleRightSelectChange}
onOpenChange={() => changeOpen('right')}
onClose={onClose}
triggerClass={classNames({
Right: true,
modal: true,
hidden: currentItemSkill?.id === 0,
})}
>
{generateRightOptions()}
</Select>
</div>
)
}
ExtendedMasterySelect.defaultProps = defaultProps
export default ExtendedMasterySelect

View file

@ -2,10 +2,10 @@
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
background-color: var(--input-bg); background-color: var(--input-bg);
border: 2px solid transparent; border: 2px solid transparent;
border-radius: 6px; border-radius: $input-corner;
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;
padding: $unit-2x; padding: calc($unit-2x - 2px);
width: 100%; width: 100%;
&[type='number']::-webkit-inner-spin-button { &[type='number']::-webkit-inner-spin-button {

View file

@ -8,7 +8,7 @@ import SelectItem from '~components/SelectItem'
import SelectGroup from '~components/SelectGroup' import SelectGroup from '~components/SelectGroup'
import { appState } from '~utils/appState' import { appState } from '~utils/appState'
import { jobGroups } from '~utils/jobGroups' import { jobGroups } from '~data/jobGroups'
import './index.scss' import './index.scss'

View file

@ -146,7 +146,7 @@ const JobSection = (props: Props) => {
ref={selectRef} ref={selectRef}
/> />
) : ( ) : (
<h3>{party.job?.name[locale]}</h3> <h3>{party.job ? party.job.name[locale] : t('no_job')}</h3>
)} )}
<ul className="JobSkills"> <ul className="JobSkills">

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { SkillGroup, skillClassification } from '~utils/skillGroups' import { SkillGroup, skillClassification } from '~data/skillGroups'
import './index.scss' import './index.scss'

View file

@ -1,6 +1,16 @@
.Login.Dialog form { .Login.DialogContent {
gap: $unit;
min-width: $unit * 52;
.DialogHeader {
padding: $unit-4x $unit-3x;
}
form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: calc($unit / 2); gap: calc($unit / 2);
margin-bottom: $unit; margin-bottom: $unit-3x;
padding: 0 $unit-3x;
}
} }

View file

@ -10,13 +10,8 @@ import { accountState } from '~utils/accountState'
import Button from '~components/Button' import Button from '~components/Button'
import Input from '~components/LabelledInput' import Input from '~components/LabelledInput'
import { import { Dialog, DialogTrigger, DialogClose } from '~components/Dialog'
Dialog, import DialogContent from '~components/DialogContent'
DialogTrigger,
DialogContent,
DialogClose,
} from '~components/Dialog'
import changeLanguage from '~utils/changeLanguage' import changeLanguage from '~utils/changeLanguage'
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from '~public/icons/Cross.svg'
@ -203,7 +198,7 @@ const LoginModal = () => {
</li> </li>
</DialogTrigger> </DialogTrigger>
<DialogContent <DialogContent
className="Login Dialog" className="Login"
onEscapeKeyDown={onEscapeKeyDown} onEscapeKeyDown={onEscapeKeyDown}
onOpenAutoFocus={onOpenAutoFocus} onOpenAutoFocus={onOpenAutoFocus}
> >

View file

@ -632,9 +632,7 @@ const PartyDetails = (props: Props) => {
<section className="DetailsWrapper"> <section className="DetailsWrapper">
<div className="PartyInfo"> <div className="PartyInfo">
<div className="Left"> <div className="Left">
<h1 className={name === '' ? 'empty' : ''}> <h1 className={name ? '' : 'empty'}>{name ? name : t('no_title')}</h1>
{name !== '' ? name : 'Untitled'}
</h1>
<div className="attribution"> <div className="attribution">
{renderUserBlock()} {renderUserBlock()}
{party.raid ? linkedRaidBlock(party.raid) : ''} {party.raid ? linkedRaidBlock(party.raid) : ''}

View file

@ -8,7 +8,7 @@ import SelectGroup from '~components/SelectGroup'
import api from '~utils/api' import api from '~utils/api'
import organizeRaids from '~utils/organizeRaids' import organizeRaids from '~utils/organizeRaids'
import { appState } from '~utils/appState' import { appState } from '~utils/appState'
import { raidGroups } from '~utils/raidGroups' import { raidGroups } from '~data/raidGroups'
import './index.scss' import './index.scss'

View file

@ -0,0 +1,5 @@
.Rings {
display: flex;
flex-direction: column;
gap: $unit;
}

View file

@ -0,0 +1,150 @@
// Core dependencies
import React, { useEffect, useState } from 'react'
// UI dependencies
import ExtendedMasterySelect from '~components/ExtendedMasterySelect'
// Data
import { overMastery } from '~data/overMastery'
// Styles and icons
import './index.scss'
// Types
import { CharacterOverMastery, ExtendedMastery } from '~types'
const emptyRing: ExtendedMastery = {
modifier: 0,
strength: 0,
}
interface Props {
gridCharacter: GridCharacter
sendValues: (overMastery: CharacterOverMastery) => void
}
const RingSelect = ({ gridCharacter, sendValues }: Props) => {
// Ring value states
const [rings, setRings] = useState<CharacterOverMastery>({
1: { ...emptyRing, modifier: 1 },
2: { ...emptyRing, modifier: 2 },
3: emptyRing,
4: emptyRing,
})
useEffect(() => {
if (gridCharacter.over_mastery) {
setRings({
1: gridCharacter.over_mastery[0],
2: gridCharacter.over_mastery[1],
3: gridCharacter.over_mastery[2],
4: gridCharacter.over_mastery[3],
})
}
}, [gridCharacter])
useEffect(() => {
sendValues(rings)
}, [rings])
function dataSet(index: number) {
const noValue = {
name: {
en: 'No over mastery bonus',
ja: 'EXリミットボーナスなし',
},
id: 0,
slug: 'no-bonus',
minValue: 0,
maxValue: 0,
suffix: '',
fractional: false,
secondary: [],
}
switch (index) {
case 1:
return overMastery.a ? [overMastery.a[0]] : []
case 2:
return overMastery.a ? [overMastery.a[1]] : []
case 3:
return overMastery.b ? [noValue, ...overMastery.b] : []
case 4:
return overMastery.c ? [noValue, ...overMastery.c] : []
default:
return []
}
}
function receiveRingValues(index: number, left: number, right: number) {
console.log(`Receiving values from ${index}: ${left} ${right}`)
if (index == 1 || index == 2) {
setSyncedRingValues(index, right)
} else if (index == 3 && left == 0) {
setRings({
...rings,
3: {
modifier: 0,
strength: 0,
},
4: {
modifier: 0,
strength: 0,
},
})
} else {
setRings({
...rings,
[index]: {
modifier: left,
strength: right,
},
})
}
}
function setSyncedRingValues(index: 1 | 2, value: number) {
console.log(`Setting synced value for ${index} with value ${value}`)
const atkValues = (dataSet(1)[0] as ItemSkill).values ?? []
const hpValues = (dataSet(2)[0] as ItemSkill).values ?? []
let found = index === 1 ? atkValues.indexOf(value) : hpValues.indexOf(value)
setRings({
...rings,
1: {
modifier: 1,
strength: atkValues[found],
},
2: {
modifier: 2,
strength: hpValues[found],
},
})
}
return (
<div className="Rings">
{[...Array(4)].map((e, i) => {
const ringIndex = i + 1
const ringStat = rings[ringIndex]
return (
<ExtendedMasterySelect
name={`ring-${ringIndex}`}
object="ring"
key={`ring-${ringIndex}`}
dataSet={dataSet(ringIndex)}
leftSelectDisabled={i === 0 || i === 1}
leftSelectValue={ringStat.modifier ? ringStat.modifier : 0}
rightSelectValue={ringStat.strength ? ringStat.strength : 0}
sendValues={(left: number, right: number) => {
receiveRingValues(ringIndex, left, right)
}}
/>
)
})}
</div>
)
}
export default RingSelect

View file

@ -1,4 +1,4 @@
.Search.Dialog { .Search.DialogContent {
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -94,7 +94,7 @@
} }
} }
.Search.Dialog #NoResults { .Search.DialogContent #NoResults {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -102,7 +102,7 @@
flex-grow: 1; flex-grow: 1;
} }
.Search.Dialog #NoResults h2 { .Search.DialogContent #NoResults h2 {
color: var(--text-secondary); color: var(--text-secondary);
font-size: $font-large; font-size: $font-large;
font-weight: 500; font-weight: 500;

View file

@ -3,16 +3,12 @@ import { getCookie, setCookie } from 'cookies-next'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import InfiniteScroll from 'react-infinite-scroll-component' import InfiniteScroll from 'react-infinite-scroll-component'
import cloneDeep from 'lodash.clonedeep'
import api from '~utils/api' import api from '~utils/api'
import { import { Dialog, DialogTrigger, DialogClose } from '~components/Dialog'
Dialog, import DialogContent from '~components/DialogContent'
DialogTrigger,
DialogContent,
DialogClose,
} from '~components/Dialog'
import Input from '~components/LabelledInput' import Input from '~components/LabelledInput'
import CharacterSearchFilterBar from '~components/CharacterSearchFilterBar' import CharacterSearchFilterBar from '~components/CharacterSearchFilterBar'
import WeaponSearchFilterBar from '~components/WeaponSearchFilterBar' import WeaponSearchFilterBar from '~components/WeaponSearchFilterBar'
@ -24,19 +20,18 @@ import WeaponResult from '~components/WeaponResult'
import SummonResult from '~components/SummonResult' import SummonResult from '~components/SummonResult'
import JobSkillResult from '~components/JobSkillResult' import JobSkillResult from '~components/JobSkillResult'
import type { DialogProps } from '@radix-ui/react-dialog'
import type { SearchableObject, SearchableObjectArray } from '~types' import type { SearchableObject, SearchableObjectArray } from '~types'
import './index.scss' import './index.scss'
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from '~public/icons/Cross.svg'
import cloneDeep from 'lodash.clonedeep'
interface Props { interface Props extends DialogProps {
send: (object: SearchableObject, position: number) => any send: (object: SearchableObject, position: number) => any
placeholderText: string placeholderText: string
fromPosition: number fromPosition: number
job?: Job job?: Job
object: 'weapons' | 'characters' | 'summons' | 'job_skills' object: 'weapons' | 'characters' | 'summons' | 'job_skills'
children: React.ReactNode
} }
const SearchModal = (props: Props) => { const SearchModal = (props: Props) => {
@ -65,6 +60,10 @@ const SearchModal = (props: Props) => {
if (searchInput.current) searchInput.current.focus() if (searchInput.current) searchInput.current.focus()
}, [searchInput]) }, [searchInput])
useEffect(() => {
if (props.open !== undefined) setOpen(props.open)
})
function inputChanged(event: React.ChangeEvent<HTMLInputElement>) { function inputChanged(event: React.ChangeEvent<HTMLInputElement>) {
const text = event.target.value const text = event.target.value
if (text.length) { if (text.length) {
@ -335,8 +334,10 @@ const SearchModal = (props: Props) => {
setRecordCount(0) setRecordCount(0)
setCurrentPage(1) setCurrentPage(1)
setOpen(false) setOpen(false)
if (props.onOpenChange) props.onOpenChange(false)
} else { } else {
setOpen(true) setOpen(true)
if (props.onOpenChange) props.onOpenChange(true)
} }
} }
@ -354,7 +355,7 @@ const SearchModal = (props: Props) => {
<Dialog open={open} onOpenChange={openChange}> <Dialog open={open} onOpenChange={openChange}>
<DialogTrigger asChild>{props.children}</DialogTrigger> <DialogTrigger asChild>{props.children}</DialogTrigger>
<DialogContent <DialogContent
className="Search Dialog" className="Search"
onEscapeKeyDown={onEscapeKeyDown} onEscapeKeyDown={onEscapeKeyDown}
onOpenAutoFocus={onOpenAutoFocus} onOpenAutoFocus={onOpenAutoFocus}
> >

View file

@ -14,6 +14,10 @@
} }
} }
&.hidden {
display: none;
}
&:hover { &:hover {
background-color: var(--input-bg-hover); background-color: var(--input-bg-hover);
color: var(--text-primary); color: var(--text-primary);
@ -24,6 +28,11 @@
} }
} }
&.Disabled:hover {
background-color: var(--input-bg);
cursor: not-allowed;
}
&[data-placeholder] > span:not(.SelectIcon) { &[data-placeholder] > span:not(.SelectIcon) {
color: var(--text-secondary); color: var(--text-secondary);
} }

View file

@ -30,6 +30,14 @@ const Select = React.forwardRef<HTMLButtonElement, Props>(function Select(
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [value, setValue] = useState('') const [value, setValue] = useState('')
const triggerClasses = classNames(
{
SelectTrigger: true,
Disabled: props.disabled,
},
props.triggerClass
)
useEffect(() => { useEffect(() => {
setOpen(props.open) setOpen(props.open)
}, [props.open]) }, [props.open])
@ -67,14 +75,18 @@ const Select = React.forwardRef<HTMLButtonElement, Props>(function Select(
onOpenChange={props.onOpenChange} onOpenChange={props.onOpenChange}
> >
<RadixSelect.Trigger <RadixSelect.Trigger
className={classNames('SelectTrigger', props.triggerClass)} className={triggerClasses}
placeholder={props.placeholder} placeholder={props.placeholder}
ref={forwardedRef} ref={forwardedRef}
> >
<RadixSelect.Value placeholder={props.placeholder} /> <RadixSelect.Value placeholder={props.placeholder} />
{!props.disabled ? (
<RadixSelect.Icon className="SelectIcon"> <RadixSelect.Icon className="SelectIcon">
<ArrowIcon /> <ArrowIcon />
</RadixSelect.Icon> </RadixSelect.Icon>
) : (
''
)}
</RadixSelect.Trigger> </RadixSelect.Trigger>
<RadixSelect.Portal className="Select"> <RadixSelect.Portal className="Select">

View file

@ -0,0 +1,29 @@
.SelectWithItem {
.InputSet {
display: flex;
flex-direction: row;
gap: $unit;
width: 100%;
.SelectTrigger {
flex-grow: 1;
width: 100%;
}
.Input {
flex-grow: 0;
text-align: right;
width: 13rem;
}
}
.errors {
color: $error;
display: none;
padding: $unit 0;
&.visible {
display: block;
}
}
}

View file

@ -0,0 +1,200 @@
// 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<ItemSkill | undefined>(undefined)
const [fieldInputValue, setFieldInputValue] = useState(inputValue)
const [error, setError] = useState('')
// Refs
const input = React.createRef<HTMLInputElement>()
// Classes
const inputClasses = classNames({
Bound: true,
Hidden: currentItemSkill?.id === 0,
})
const errorClasses = classNames({
errors: true,
visible: error !== '',
})
// Hooks
// Set default values from props
useEffect(() => {
const found = dataSet.find((sk) => sk.id === selectValue)
if (found) {
setCurrentItemSkill(found)
setFieldInputValue(inputValue)
}
}, [selectValue, inputValue])
// Methods: UI state management
function changeOpen() {
if (!selectDisabled) {
setOpen(!open)
if (onOpenChange) onOpenChange(!open)
}
}
function onClose() {
if (onOpenChange) onOpenChange(false)
}
// Methods: Rendering
function generateOptions() {
let options: React.ReactNode[] = dataSet.map((skill, i) => {
return (
<SelectItem key={i} value={skill.id}>
{skill.name[locale]}
</SelectItem>
)
})
return options
}
// Methods: User input detection
function handleSelectChange(rawValue: string) {
const value = parseInt(rawValue)
const skill = dataSet.find((sk) => sk.id === value)
if (skill) {
setCurrentItemSkill(skill)
sendValues(skill.id, fieldInputValue)
}
}
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = parseFloat(event.target.value)
if (handleInputError(value)) setFieldInputValue(value)
if (currentItemSkill) sendValues(currentItemSkill.id, 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 (
<div className="SelectWithItem">
<div className="InputSet">
<Select
key={`${currentItemSkill?.name.en}_type`}
value={`${currentItemSkill ? currentItemSkill.id : 0}`}
open={open}
disabled={selectDisabled}
onValueChange={handleSelectChange}
onOpenChange={changeOpen}
onClose={onClose}
triggerClass="modal"
>
{generateOptions()}
</Select>
<Input
value={fieldInputValue}
className={inputClasses}
type="number"
placeholder={rangeString()}
min={currentItemSkill?.minValue}
max={currentItemSkill?.maxValue}
step="1"
onChange={handleInputChange}
visible={currentItemSkill ? 'true' : 'false'}
ref={input}
/>
</div>
<p className={errorClasses}>{error}</p>
</div>
)
}
SelectWithInput.defaultProps = defaultProps
export default SelectWithInput

View file

@ -1,8 +1,17 @@
.Signup.Dialog form { .Signup.DialogContent {
gap: $unit;
min-width: $unit * 52;
.DialogHeader {
padding: $unit-4x $unit-3x;
}
form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: calc($unit / 2); gap: calc($unit / 2);
margin-bottom: $unit; margin-bottom: $unit-2x;
padding: 0 $unit-3x;
.terms { .terms {
color: $grey-50; color: $grey-50;
@ -20,3 +29,4 @@
} }
} }
} }
}

View file

@ -10,13 +10,8 @@ import { accountState } from '~utils/accountState'
import Button from '~components/Button' import Button from '~components/Button'
import Input from '~components/LabelledInput' import Input from '~components/LabelledInput'
import { import { Dialog, DialogTrigger, DialogClose } from '~components/Dialog'
Dialog, import DialogContent from '~components/DialogContent'
DialogTrigger,
DialogContent,
DialogClose,
} from '~components/Dialog'
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from '~public/icons/Cross.svg'
import './index.scss' import './index.scss'
@ -283,7 +278,7 @@ const SignupModal = (props: Props) => {
</li> </li>
</DialogTrigger> </DialogTrigger>
<DialogContent <DialogContent
className="Signup Dialog" className="Signup"
onEscapeKeyDown={onEscapeKeyDown} onEscapeKeyDown={onEscapeKeyDown}
onOpenAutoFocus={onOpenAutoFocus} onOpenAutoFocus={onOpenAutoFocus}
> >

View file

@ -42,7 +42,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: hidden; overflow: hidden;
transition: all 0.18s ease-in-out; transition: $duration-zoom all ease-in-out;
&:hover .icon svg { &:hover .icon svg {
fill: var(--icon-secondary-hover); fill: var(--icon-secondary-hover);
@ -55,6 +55,7 @@
z-index: 1; z-index: 1;
svg { svg {
transition: $duration-color-fade fill ease-in-out;
fill: var(--icon-secondary); fill: var(--icon-secondary);
} }
} }

View file

@ -2,7 +2,8 @@ import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { Dialog, DialogContent } from '~components/Dialog' import { Dialog } from '~components/Dialog'
import DialogContent from '~components/DialogContent'
import Button from '~components/Button' import Button from '~components/Button'
import Overlay from '~components/Overlay' import Overlay from '~components/Overlay'
@ -65,7 +66,7 @@ const WeaponConflictModal = (props: Props) => {
return ( return (
<Dialog open={open} onOpenChange={openChange}> <Dialog open={open} onOpenChange={openChange}>
<DialogContent <DialogContent
className="Conflict Dialog" className="Conflict"
onOpenAutoFocus={(event) => event.preventDefault()} onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={close} onEscapeKeyDown={close}
> >

View file

@ -7,18 +7,18 @@ import { useTranslation } from 'next-i18next'
import { AxiosResponse } from 'axios' import { AxiosResponse } from 'axios'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import Alert from '~components/Alert'
import WeaponUnit from '~components/WeaponUnit' import WeaponUnit from '~components/WeaponUnit'
import ExtraWeapons from '~components/ExtraWeapons' import ExtraWeapons from '~components/ExtraWeapons'
import WeaponConflictModal from '~components/WeaponConflictModal'
import api from '~utils/api' import api from '~utils/api'
import { appState } from '~utils/appState' import { appState } from '~utils/appState'
import { accountState } from '~utils/accountState'
import type { DetailsObject, SearchableObject } from '~types' import type { DetailsObject, SearchableObject } from '~types'
import './index.scss' import './index.scss'
import WeaponConflictModal from '~components/WeaponConflictModal'
import Alert from '~components/Alert'
import { accountState } from '~utils/accountState'
// Props // Props
interface Props { interface Props {
@ -198,6 +198,21 @@ const WeaponGrid = (props: Props) => {
setIncoming(undefined) setIncoming(undefined)
} }
async function removeWeapon(id: string) {
try {
const response = await api.endpoints.grid_weapons.destroy({ id: id })
const data = response.data
if (data.position === -1) {
appState.grid.weapons.mainWeapon = undefined
} else {
appState.grid.weapons.allWeapons[response.data.position] = undefined
}
} catch (error) {
console.error(error)
}
}
// Methods: Updating uncap level // Methods: Updating uncap level
// Note: Saves, but debouncing is not working properly // Note: Saves, but debouncing is not working properly
async function saveUncap(id: string, position: number, uncapLevel: number) { async function saveUncap(id: string, position: number, uncapLevel: number) {
@ -254,7 +269,7 @@ const WeaponGrid = (props: Props) => {
) )
const updateUncapLevel = (position: number, uncapLevel: number) => { const updateUncapLevel = (position: number, uncapLevel: number) => {
console.log(`Updating uncap level at position ${position} to ${uncapLevel}`) // console.log(`Updating uncap level at position ${position} to ${uncapLevel}`)
if (appState.grid.weapons.mainWeapon && position == -1) if (appState.grid.weapons.mainWeapon && position == -1)
appState.grid.weapons.mainWeapon.uncap_level = uncapLevel appState.grid.weapons.mainWeapon.uncap_level = uncapLevel
else { else {
@ -292,6 +307,7 @@ const WeaponGrid = (props: Props) => {
key="grid_mainhand" key="grid_mainhand"
position={-1} position={-1}
unitType={0} unitType={0}
removeWeapon={removeWeapon}
updateObject={receiveWeaponFromSearch} updateObject={receiveWeaponFromSearch}
updateUncap={initiateUncapUpdate} updateUncap={initiateUncapUpdate}
/> />
@ -305,6 +321,7 @@ const WeaponGrid = (props: Props) => {
editable={party.editable} editable={party.editable}
position={i} position={i}
unitType={1} unitType={1}
removeWeapon={removeWeapon}
updateObject={receiveWeaponFromSearch} updateObject={receiveWeaponFromSearch}
updateUncap={initiateUncapUpdate} updateUncap={initiateUncapUpdate}
/> />

View file

@ -7,7 +7,7 @@ import * as HoverCard from '@radix-ui/react-hover-card'
import WeaponLabelIcon from '~components/WeaponLabelIcon' import WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from '~components/UncapIndicator' import UncapIndicator from '~components/UncapIndicator'
import { axData } from '~utils/axData' import ax from '~data/ax'
import './index.scss' import './index.scss'
@ -80,7 +80,7 @@ const WeaponHovercard = (props: Props) => {
} }
const createPrimaryAxSkillString = () => { const createPrimaryAxSkillString = () => {
const primaryAxSkills = axData[props.gridWeapon.object.ax_type - 1] const primaryAxSkills = ax[props.gridWeapon.object.ax_type - 1]
if (props.gridWeapon.ax) { if (props.gridWeapon.ax) {
const simpleAxSkill = props.gridWeapon.ax[0] const simpleAxSkill = props.gridWeapon.ax[0]
@ -97,7 +97,7 @@ const WeaponHovercard = (props: Props) => {
} }
const createSecondaryAxSkillString = () => { const createSecondaryAxSkillString = () => {
const primaryAxSkills = axData[props.gridWeapon.object.ax_type - 1] const primaryAxSkills = ax[props.gridWeapon.object.ax_type - 1]
if (props.gridWeapon.ax) { if (props.gridWeapon.ax) {
const primarySimpleAxSkill = props.gridWeapon.ax[0] const primarySimpleAxSkill = props.gridWeapon.ax[0]

View file

@ -1,14 +1,41 @@
.Weapon.Dialog { .Weapon.DialogContent {
gap: $unit;
min-width: 480px; min-width: 480px;
@include breakpoint(phone) { @include breakpoint(phone) {
min-width: inherit; min-width: inherit;
} }
.DialogHeader {
transition: 0.18s padding-top ease-in-out;
position: sticky;
top: 0;
&.Scrolled {
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 12px rgba(0, 0, 0, 0.34);
padding-top: $unit-2x;
}
img {
transition: 0.2s width ease-in-out;
width: $unit-6x !important;
}
.DialogTitle {
font-size: $font-large;
}
.SubTitle {
display: none;
}
}
.mods { .mods {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $unit * 4; gap: $unit * 4;
padding: 0 $unit-4x;
section { section {
display: flex; display: flex;

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react' import React, { PropsWithChildren, useEffect, useState } from 'react'
import { getCookie } from 'cookies-next' import { getCookie } from 'cookies-next'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
@ -7,11 +7,10 @@ import { AxiosResponse } from 'axios'
import { import {
Dialog, Dialog,
DialogClose, DialogClose,
DialogContent,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '~components/Dialog' } from '~components/Dialog'
import DialogContent from '~components/DialogContent'
import AXSelect from '~components/AxSelect' import AXSelect from '~components/AxSelect'
import AwakeningSelect from '~components/AwakeningSelect' import AwakeningSelect from '~components/AwakeningSelect'
import ElementToggle from '~components/ElementToggle' import ElementToggle from '~components/ElementToggle'
@ -41,10 +40,16 @@ interface GridWeaponObject {
interface Props { interface Props {
gridWeapon: GridWeapon gridWeapon: GridWeapon
children: React.ReactNode open: boolean
onOpenChange: (open: boolean) => void
} }
const WeaponModal = (props: Props) => { const WeaponModal = ({
gridWeapon,
open: modalOpen,
children,
onOpenChange,
}: PropsWithChildren<Props>) => {
const router = useRouter() const router = useRouter()
const locale = const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
@ -65,7 +70,7 @@ const WeaponModal = (props: Props) => {
const [element, setElement] = useState(-1) const [element, setElement] = useState(-1)
const [awakeningType, setAwakeningType] = useState(-1) const [awakeningType, setAwakeningType] = useState(0)
const [awakeningLevel, setAwakeningLevel] = useState(1) const [awakeningLevel, setAwakeningLevel] = useState(1)
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1) const [primaryAxModifier, setPrimaryAxModifier] = useState(-1)
@ -89,10 +94,14 @@ const WeaponModal = (props: Props) => {
const [awakeningOpen, setAwakeningOpen] = useState(false) const [awakeningOpen, setAwakeningOpen] = useState(false)
useEffect(() => { useEffect(() => {
setElement(props.gridWeapon.element) setOpen(modalOpen)
}, [modalOpen])
if (props.gridWeapon.weapon_keys) { useEffect(() => {
props.gridWeapon.weapon_keys.forEach((key) => { setElement(gridWeapon.element)
if (gridWeapon.weapon_keys) {
gridWeapon.weapon_keys.forEach((key) => {
if (key.slot + 1 === 1) { if (key.slot + 1 === 1) {
setWeaponKey1(key) setWeaponKey1(key)
} else if (key.slot + 1 === 2) { } else if (key.slot + 1 === 2) {
@ -102,7 +111,7 @@ const WeaponModal = (props: Props) => {
} }
}) })
} }
}, [props]) }, [gridWeapon])
function receiveAxValues( function receiveAxValues(
primaryAxModifier: number, primaryAxModifier: number,
@ -133,29 +142,26 @@ const WeaponModal = (props: Props) => {
function prepareObject() { function prepareObject() {
let object: GridWeaponObject = { weapon: {} } let object: GridWeaponObject = { weapon: {} }
if (props.gridWeapon.object.element == 0) object.weapon.element = element if (gridWeapon.object.element == 0) object.weapon.element = element
if ( if ([2, 3, 17, 24].includes(gridWeapon.object.series) && weaponKey1Id) {
[2, 3, 17, 24].includes(props.gridWeapon.object.series) &&
weaponKey1Id
) {
object.weapon.weapon_key1_id = weaponKey1Id object.weapon.weapon_key1_id = weaponKey1Id
} }
if ([2, 3, 17].includes(props.gridWeapon.object.series) && weaponKey2Id) if ([2, 3, 17].includes(gridWeapon.object.series) && weaponKey2Id)
object.weapon.weapon_key2_id = weaponKey2Id object.weapon.weapon_key2_id = weaponKey2Id
if (props.gridWeapon.object.series == 17 && weaponKey3Id) if (gridWeapon.object.series == 17 && weaponKey3Id)
object.weapon.weapon_key3_id = weaponKey3Id object.weapon.weapon_key3_id = weaponKey3Id
if (props.gridWeapon.object.ax && props.gridWeapon.object.ax_type > 0) { if (gridWeapon.object.ax && gridWeapon.object.ax_type > 0) {
object.weapon.ax_modifier1 = primaryAxModifier object.weapon.ax_modifier1 = primaryAxModifier
object.weapon.ax_modifier2 = secondaryAxModifier object.weapon.ax_modifier2 = secondaryAxModifier
object.weapon.ax_strength1 = primaryAxValue object.weapon.ax_strength1 = primaryAxValue
object.weapon.ax_strength2 = secondaryAxValue object.weapon.ax_strength2 = secondaryAxValue
} }
if (props.gridWeapon.object.awakening) { if (gridWeapon.object.awakening) {
object.weapon.awakening_type = awakeningType object.weapon.awakening_type = awakeningType
object.weapon.awakening_level = awakeningLevel object.weapon.awakening_level = awakeningLevel
} }
@ -166,7 +172,7 @@ const WeaponModal = (props: Props) => {
async function updateWeapon() { async function updateWeapon() {
const updateObject = prepareObject() const updateObject = prepareObject()
return await api.endpoints.grid_weapons return await api.endpoints.grid_weapons
.update(props.gridWeapon.id, updateObject, headers) .update(gridWeapon.id, updateObject, headers)
.then((response) => processResult(response)) .then((response) => processResult(response))
.catch((error) => processError(error)) .catch((error) => processError(error))
} }
@ -222,11 +228,11 @@ const WeaponModal = (props: Props) => {
return ( return (
<section> <section>
<h3>{t('modals.weapon.subtitles.weapon_keys')}</h3> <h3>{t('modals.weapon.subtitles.weapon_keys')}</h3>
{[2, 3, 17, 22].includes(props.gridWeapon.object.series) ? ( {[2, 3, 17, 22].includes(gridWeapon.object.series) ? (
<WeaponKeySelect <WeaponKeySelect
open={weaponKey1Open} open={weaponKey1Open}
currentValue={weaponKey1 != null ? weaponKey1 : undefined} currentValue={weaponKey1 != null ? weaponKey1 : undefined}
series={props.gridWeapon.object.series} series={gridWeapon.object.series}
slot={0} slot={0}
onOpenChange={() => openSelect(1)} onOpenChange={() => openSelect(1)}
onChange={receiveWeaponKey} onChange={receiveWeaponKey}
@ -236,11 +242,11 @@ const WeaponModal = (props: Props) => {
'' ''
)} )}
{[2, 3, 17].includes(props.gridWeapon.object.series) ? ( {[2, 3, 17].includes(gridWeapon.object.series) ? (
<WeaponKeySelect <WeaponKeySelect
open={weaponKey2Open} open={weaponKey2Open}
currentValue={weaponKey2 != null ? weaponKey2 : undefined} currentValue={weaponKey2 != null ? weaponKey2 : undefined}
series={props.gridWeapon.object.series} series={gridWeapon.object.series}
slot={1} slot={1}
onOpenChange={() => openSelect(2)} onOpenChange={() => openSelect(2)}
onChange={receiveWeaponKey} onChange={receiveWeaponKey}
@ -250,11 +256,11 @@ const WeaponModal = (props: Props) => {
'' ''
)} )}
{props.gridWeapon.object.series == 17 ? ( {gridWeapon.object.series == 17 ? (
<WeaponKeySelect <WeaponKeySelect
open={weaponKey3Open} open={weaponKey3Open}
currentValue={weaponKey3 != null ? weaponKey3 : undefined} currentValue={weaponKey3 != null ? weaponKey3 : undefined}
series={props.gridWeapon.object.series} series={gridWeapon.object.series}
slot={2} slot={2}
onOpenChange={() => openSelect(3)} onOpenChange={() => openSelect(3)}
onChange={receiveWeaponKey} onChange={receiveWeaponKey}
@ -264,12 +270,11 @@ const WeaponModal = (props: Props) => {
'' ''
)} )}
{props.gridWeapon.object.series == 24 && {gridWeapon.object.series == 24 && gridWeapon.object.uncap.ulb ? (
props.gridWeapon.object.uncap.ulb ? (
<WeaponKeySelect <WeaponKeySelect
open={weaponKey4Open} open={weaponKey4Open}
currentValue={weaponKey1 != null ? weaponKey1 : undefined} currentValue={weaponKey1 != null ? weaponKey1 : undefined}
series={props.gridWeapon.object.series} series={gridWeapon.object.series}
slot={0} slot={0}
onOpenChange={() => openSelect(4)} onOpenChange={() => openSelect(4)}
onChange={receiveWeaponKey} onChange={receiveWeaponKey}
@ -287,8 +292,8 @@ const WeaponModal = (props: Props) => {
<section> <section>
<h3>{t('modals.weapon.subtitles.ax_skills')}</h3> <h3>{t('modals.weapon.subtitles.ax_skills')}</h3>
<AXSelect <AXSelect
axType={props.gridWeapon.object.ax_type} axType={gridWeapon.object.ax_type}
currentSkills={props.gridWeapon.ax} currentSkills={gridWeapon.ax}
onOpenChange={receiveAxOpen} onOpenChange={receiveAxOpen}
sendValidity={receiveValidity} sendValidity={receiveValidity}
sendValues={receiveAxValues} sendValues={receiveAxValues}
@ -303,8 +308,8 @@ const WeaponModal = (props: Props) => {
<h3>{t('modals.weapon.subtitles.awakening')}</h3> <h3>{t('modals.weapon.subtitles.awakening')}</h3>
<AwakeningSelect <AwakeningSelect
object="weapon" object="weapon"
awakeningType={props.gridWeapon.awakening?.type} type={gridWeapon.awakening?.type}
awakeningLevel={props.gridWeapon.awakening?.level} level={gridWeapon.awakening?.level}
onOpenChange={receiveAwakeningOpen} onOpenChange={receiveAwakeningOpen}
sendValidity={receiveValidity} sendValidity={receiveValidity}
sendValues={receiveAwakeningValues} sendValues={receiveAwakeningValues}
@ -313,13 +318,14 @@ const WeaponModal = (props: Props) => {
) )
} }
function openChange(open: boolean) { function handleOpenChange(open: boolean) {
if (props.gridWeapon.object.ax || props.gridWeapon.object.awakening) { if (gridWeapon.object.ax || gridWeapon.object.awakening) {
setFormValid(false) setFormValid(false)
} else { } else {
setFormValid(true) setFormValid(true)
} }
setOpen(open) setOpen(open)
onOpenChange(open)
} }
const anySelectOpen = const anySelectOpen =
@ -341,20 +347,25 @@ const WeaponModal = (props: Props) => {
return ( return (
// TODO: Refactor into Dialog component // TODO: Refactor into Dialog component
<Dialog open={open} onOpenChange={openChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>{props.children}</DialogTrigger> <DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent <DialogContent
className="Weapon Dialog" className="Weapon"
onOpenAutoFocus={(event) => event.preventDefault()} onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={onEscapeKeyDown} onEscapeKeyDown={onEscapeKeyDown}
> >
<div className="DialogHeader"> <div className="DialogHeader">
<img
alt={gridWeapon.object.name[locale]}
className="DialogImage"
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-square/${gridWeapon.object.granblue_id}.jpg`}
/>
<div className="DialogTop"> <div className="DialogTop">
<DialogTitle className="SubTitle"> <DialogTitle className="SubTitle">
{t('modals.weapon.title')} {t('modals.weapon.title')}
</DialogTitle> </DialogTitle>
<DialogTitle className="DialogTitle"> <DialogTitle className="DialogTitle">
{props.gridWeapon.object.name[locale]} {gridWeapon.object.name[locale]}
</DialogTitle> </DialogTitle>
</div> </div>
<DialogClose className="DialogClose" asChild> <DialogClose className="DialogClose" asChild>
@ -365,12 +376,12 @@ const WeaponModal = (props: Props) => {
</div> </div>
<div className="mods"> <div className="mods">
{props.gridWeapon.object.element == 0 ? elementSelect() : ''} {gridWeapon.object.element == 0 ? elementSelect() : ''}
{[2, 3, 17, 24].includes(props.gridWeapon.object.series) {[2, 3, 17, 24].includes(gridWeapon.object.series) ? keySelect() : ''}
? keySelect() {gridWeapon.object.ax ? axSelect() : ''}
: ''} {gridWeapon.awakening ? awakeningSelect() : ''}
{props.gridWeapon.object.ax ? axSelect() : ''} </div>
{props.gridWeapon.awakening ? awakeningSelect() : ''} <div className="DialogFooter">
<Button <Button
contained={true} contained={true}
onClick={updateWeapon} onClick={updateWeapon}

View file

@ -16,7 +16,7 @@ import {
emptyWeaponSeriesState, emptyWeaponSeriesState,
} from '~utils/emptyStates' } from '~utils/emptyStates'
import { elements, proficiencies, rarities } from '~utils/stateValues' import { elements, proficiencies, rarities } from '~utils/stateValues'
import { weaponSeries } from '~utils/weaponSeries' import { weaponSeries } from '~data/weaponSeries'
interface Props { interface Props {
sendFilters: (filters: { [key: string]: number[] }) => void sendFilters: (filters: { [key: string]: number[] }) => void

View file

@ -12,8 +12,16 @@
min-height: auto; min-height: auto;
} }
&:hover .Button { .Button {
display: block; pointer-events: none;
opacity: 0;
z-index: 10;
}
&:hover .Button,
.Button.Clicked {
pointer-events: initial;
opacity: 1;
} }
&.editable .WeaponImage:hover { &.editable .WeaponImage:hover {
@ -95,15 +103,6 @@
display: none; display: none;
} }
.Button {
box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.14);
display: none;
position: absolute;
left: $unit;
top: $unit;
z-index: 10;
}
h3 { h3 {
color: var(--text-primary); color: var(--text-primary);
font-size: $font-button; font-size: $font-button;
@ -123,7 +122,7 @@
margin-bottom: calc($unit / 4); margin-bottom: calc($unit / 4);
overflow: hidden; overflow: hidden;
position: relative; position: relative;
transition: all 0.18s ease-in-out; transition: $duration-zoom all ease-in-out;
&:hover .icon svg { &:hover .icon svg {
fill: var(--icon-secondary-hover); fill: var(--icon-secondary-hover);
@ -170,6 +169,7 @@
z-index: 1; z-index: 1;
svg { svg {
transition: $duration-color-fade fill ease-in-out;
fill: var(--icon-secondary); fill: var(--icon-secondary);
} }
} }

View file

@ -1,18 +1,25 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState, MouseEvent } from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next' import { Trans, useTranslation } from 'next-i18next'
import classNames from 'classnames' import classNames from 'classnames'
import Alert from '~components/Alert'
import SearchModal from '~components/SearchModal' import SearchModal from '~components/SearchModal'
import WeaponModal from '~components/WeaponModal' import WeaponModal from '~components/WeaponModal'
import {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
} from '~components/ContextMenu'
import ContextMenuItem from '~components/ContextMenuItem'
import WeaponHovercard from '~components/WeaponHovercard' import WeaponHovercard from '~components/WeaponHovercard'
import UncapIndicator from '~components/UncapIndicator' import UncapIndicator from '~components/UncapIndicator'
import Button from '~components/Button' import Button from '~components/Button'
import type { SearchableObject } from '~types' import type { SearchableObject } from '~types'
import { axData } from '~utils/axData' import ax from '~data/ax'
import { weaponAwakening } from '~utils/awakening' import { weaponAwakening } from '~data/awakening'
import PlusIcon from '~public/icons/Add.svg' import PlusIcon from '~public/icons/Add.svg'
import SettingsIcon from '~public/icons/Settings.svg' import SettingsIcon from '~public/icons/Settings.svg'
@ -23,48 +30,147 @@ interface Props {
unitType: 0 | 1 unitType: 0 | 1
position: number position: number
editable: boolean editable: boolean
removeWeapon: (id: string) => void
updateObject: (object: SearchableObject, position: number) => void updateObject: (object: SearchableObject, position: number) => void
updateUncap: (id: string, position: number, uncap: number) => void updateUncap: (id: string, position: number, uncap: number) => void
} }
const WeaponUnit = (props: Props) => { const WeaponUnit = ({
gridWeapon,
unitType,
position,
editable,
removeWeapon: sendWeaponToRemove,
updateObject,
updateUncap,
}: Props) => {
// Translations and locale
const { t } = useTranslation('common') const { t } = useTranslation('common')
const [imageUrl, setImageUrl] = useState('')
const router = useRouter() const router = useRouter()
const locale = const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
// State: UI
const [detailsModalOpen, setDetailsModalOpen] = useState(false)
const [searchModalOpen, setSearchModalOpen] = useState(false)
const [contextMenuOpen, setContextMenuOpen] = useState(false)
const [alertOpen, setAlertOpen] = useState(false)
// State: Other
const [imageUrl, setImageUrl] = useState('')
// Classes
const classes = classNames({ const classes = classNames({
WeaponUnit: true, WeaponUnit: true,
mainhand: props.unitType == 0, mainhand: unitType == 0,
grid: props.unitType == 1, grid: unitType == 1,
editable: props.editable, editable: editable,
filled: props.gridWeapon !== undefined, filled: gridWeapon !== undefined,
empty: props.gridWeapon == undefined, empty: gridWeapon == undefined,
}) })
const gridWeapon = props.gridWeapon const buttonClasses = classNames({
Options: true,
Clicked: contextMenuOpen,
})
// Other
const weapon = gridWeapon?.object const weapon = gridWeapon?.object
// Hooks
useEffect(() => { useEffect(() => {
generateImageUrl() generateImageUrl()
}) })
// Methods: Convenience
function canBeModified(gridWeapon: GridWeapon) {
const weapon = gridWeapon.object
return (
weapon.ax ||
weapon.awakening ||
(weapon.series && [2, 3, 17, 22, 24].includes(weapon.series))
)
}
// Methods: Open layer
function openWeaponModal(event: Event) {
setDetailsModalOpen(true)
}
function openSearchModal(event: MouseEvent<HTMLDivElement>) {
if (editable) setSearchModalOpen(true)
}
function openRemoveWeaponAlert() {
setAlertOpen(true)
}
// Methods: Handle button clicked
function handleButtonClicked() {
setContextMenuOpen(!contextMenuOpen)
}
// Methods: Handle open change
function handleContextMenuOpenChange(open: boolean) {
if (!open) setContextMenuOpen(false)
}
function handleWeaponModalOpenChange(open: boolean) {
setDetailsModalOpen(open)
}
function handleSearchModalOpenChange(open: boolean) {
setSearchModalOpen(open)
}
// Methods: Mutate data
function passUncapData(index: number) {
if (gridWeapon) updateUncap(gridWeapon.id, position, index)
}
function removeWeapon() {
if (gridWeapon) sendWeaponToRemove(gridWeapon.id)
}
// Methods: Data fetching and manipulation
function getCanonicalAxSkill(index: number) {
if (
gridWeapon &&
gridWeapon.object.ax &&
gridWeapon.object.ax_type > 0 &&
gridWeapon.ax
) {
const axOptions = ax[gridWeapon.object.ax_type - 1]
const weaponAxSkill: SimpleAxSkill = gridWeapon.ax[0]
let axSkill = axOptions.find((ax) => ax.id === weaponAxSkill.modifier)
if (index !== 0 && axSkill && axSkill.secondary) {
const weaponSubAxSkill: SimpleAxSkill = gridWeapon.ax[1]
axSkill = axSkill.secondary.find(
(ax) => ax.id === weaponSubAxSkill.modifier
)
}
return axSkill
} else return
}
// Methods: Image string generation
function generateImageUrl() { function generateImageUrl() {
let imgSrc = '' let imgSrc = ''
if (props.gridWeapon) { if (gridWeapon) {
const weapon = props.gridWeapon.object! const weapon = gridWeapon.object!
if (props.unitType == 0) { if (unitType == 0) {
if (props.gridWeapon.object.element == 0 && props.gridWeapon.element) if (gridWeapon.object.element == 0 && gridWeapon.element)
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}_${props.gridWeapon.element}.jpg` imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}_${gridWeapon.element}.jpg`
else else
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg` imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg`
} else { } else {
if (props.gridWeapon.object.element == 0 && props.gridWeapon.element) if (gridWeapon.object.element == 0 && gridWeapon.element)
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${props.gridWeapon.element}.jpg` imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${gridWeapon.element}.jpg`
else else
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg` imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`
} }
@ -74,29 +180,30 @@ const WeaponUnit = (props: Props) => {
} }
function placeholderImageUrl() { function placeholderImageUrl() {
return props.unitType == 0 return unitType == 0
? '/images/placeholders/placeholder-weapon-main.png' ? '/images/placeholders/placeholder-weapon-main.png'
: '/images/placeholders/placeholder-weapon-grid.png' : '/images/placeholders/placeholder-weapon-grid.png'
} }
// Methods: Image element rendering
function awakeningImage() { function awakeningImage() {
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.awakening && gridWeapon.object.awakening &&
props.gridWeapon.awakening && gridWeapon.awakening &&
props.gridWeapon.awakening.type >= 0 && gridWeapon.awakening.type > 0 &&
props.gridWeapon.awakening.type != null gridWeapon.awakening.type != null
) { ) {
const awakening = weaponAwakening.find( const awakening = weaponAwakening.find(
(awakening) => awakening.id === props.gridWeapon?.awakening?.type (awakening) => awakening.id === gridWeapon?.awakening?.type
) )
const name = awakening?.name[locale] const name = awakening?.name[locale]
return ( return (
<img <img
alt={`${name} Lv${props.gridWeapon.awakening.level}`} alt={`${name} Lv${gridWeapon.awakening.level}`}
className="Awakening" className="Awakening"
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/awakening/weapon_${props.gridWeapon.awakening.type}.png`} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/awakening/weapon_${gridWeapon.awakening.type}.png`}
/> />
) )
} }
@ -109,18 +216,18 @@ const WeaponUnit = (props: Props) => {
// If there is a grid weapon, it is a Draconic Weapon and it has keys // If there is a grid weapon, it is a Draconic Weapon and it has keys
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.series === 3 && gridWeapon.object.series === 3 &&
props.gridWeapon.weapon_keys gridWeapon.weapon_keys
) { ) {
if (index === 0 && props.gridWeapon.weapon_keys[0]) { if (index === 0 && gridWeapon.weapon_keys[0]) {
altText = `${props.gridWeapon.weapon_keys[0].name[locale]}` altText = `${gridWeapon.weapon_keys[0].name[locale]}`
filename = `${props.gridWeapon.weapon_keys[0].slug}.png` filename = `${gridWeapon.weapon_keys[0].slug}.png`
} else if (index === 1 && props.gridWeapon.weapon_keys[1]) { } else if (index === 1 && gridWeapon.weapon_keys[1]) {
altText = `${props.gridWeapon.weapon_keys[1].name[locale]}` altText = `${gridWeapon.weapon_keys[1].name[locale]}`
const element = props.gridWeapon.object.element const element = gridWeapon.object.element
filename = `${props.gridWeapon.weapon_keys[1].slug}-${element}.png` filename = `${gridWeapon.weapon_keys[1].slug}-${element}.png`
} }
return ( return (
@ -137,12 +244,12 @@ const WeaponUnit = (props: Props) => {
function telumaImages() { function telumaImages() {
let images: JSX.Element[] = [] let images: JSX.Element[] = []
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.series === 3 && gridWeapon.object.series === 3 &&
props.gridWeapon.weapon_keys && gridWeapon.weapon_keys &&
props.gridWeapon.weapon_keys.length > 0 gridWeapon.weapon_keys.length > 0
) { ) {
for (let i = 0; i < props.gridWeapon.weapon_keys.length; i++) { for (let i = 0; i < gridWeapon.weapon_keys.length; i++) {
const image = telumaImage(i) const image = telumaImage(i)
if (image) images.push(image) if (image) images.push(image)
} }
@ -158,27 +265,27 @@ const WeaponUnit = (props: Props) => {
// If there is a grid weapon, it is a Dark Opus Weapon and it has keys // If there is a grid weapon, it is a Dark Opus Weapon and it has keys
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.series === 17 && gridWeapon.object.series === 17 &&
props.gridWeapon.weapon_keys gridWeapon.weapon_keys
) { ) {
if ( if (
props.gridWeapon.weapon_keys[index] && gridWeapon.weapon_keys[index] &&
(props.gridWeapon.weapon_keys[index].slot === 1 || (gridWeapon.weapon_keys[index].slot === 1 ||
props.gridWeapon.weapon_keys[index].slot === 2) gridWeapon.weapon_keys[index].slot === 2)
) { ) {
altText = `${props.gridWeapon.weapon_keys[index].name[locale]}` altText = `${gridWeapon.weapon_keys[index].name[locale]}`
filename = `${props.gridWeapon.weapon_keys[index].slug}.png` filename = `${gridWeapon.weapon_keys[index].slug}.png`
} else if ( } else if (
props.gridWeapon.weapon_keys[index] && gridWeapon.weapon_keys[index] &&
props.gridWeapon.weapon_keys[index].slot === 0 gridWeapon.weapon_keys[index].slot === 0
) { ) {
altText = `${props.gridWeapon.weapon_keys[index].name[locale]}` altText = `${gridWeapon.weapon_keys[index].name[locale]}`
const weapon = props.gridWeapon.object.proficiency const weapon = gridWeapon.object.proficiency
const suffix = `${weapon}` const suffix = `${weapon}`
filename = `${props.gridWeapon.weapon_keys[index].slug}-${suffix}.png` filename = `${gridWeapon.weapon_keys[index].slug}-${suffix}.png`
} }
} }
@ -195,12 +302,12 @@ const WeaponUnit = (props: Props) => {
function ultimaImages() { function ultimaImages() {
let images: JSX.Element[] = [] let images: JSX.Element[] = []
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.series === 17 && gridWeapon.object.series === 17 &&
props.gridWeapon.weapon_keys && gridWeapon.weapon_keys &&
props.gridWeapon.weapon_keys.length > 0 gridWeapon.weapon_keys.length > 0
) { ) {
for (let i = 0; i < props.gridWeapon.weapon_keys.length; i++) { for (let i = 0; i < gridWeapon.weapon_keys.length; i++) {
const image = ultimaImage(i) const image = ultimaImage(i)
if (image) images.push(image) if (image) images.push(image)
} }
@ -216,29 +323,29 @@ const WeaponUnit = (props: Props) => {
// If there is a grid weapon, it is a Dark Opus Weapon and it has keys // If there is a grid weapon, it is a Dark Opus Weapon and it has keys
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.series === 2 && gridWeapon.object.series === 2 &&
props.gridWeapon.weapon_keys gridWeapon.weapon_keys
) { ) {
if ( if (
props.gridWeapon.weapon_keys[index] && gridWeapon.weapon_keys[index] &&
props.gridWeapon.weapon_keys[index].slot === 0 gridWeapon.weapon_keys[index].slot === 0
) { ) {
altText = `${props.gridWeapon.weapon_keys[index].name[locale]}` altText = `${gridWeapon.weapon_keys[index].name[locale]}`
filename = `${props.gridWeapon.weapon_keys[index].slug}.png` filename = `${gridWeapon.weapon_keys[index].slug}.png`
} else if ( } else if (
props.gridWeapon.weapon_keys[index] && gridWeapon.weapon_keys[index] &&
props.gridWeapon.weapon_keys[index].slot === 1 gridWeapon.weapon_keys[index].slot === 1
) { ) {
altText = `${props.gridWeapon.weapon_keys[index].name[locale]}` altText = `${gridWeapon.weapon_keys[index].name[locale]}`
const element = props.gridWeapon.object.element const element = gridWeapon.object.element
const mod = props.gridWeapon.object.name.en.includes('Repudiation') const mod = gridWeapon.object.name.en.includes('Repudiation')
? 'primal' ? 'primal'
: 'magna' : 'magna'
const suffix = `${mod}-${element}` const suffix = `${mod}-${element}`
const weaponKey = props.gridWeapon.weapon_keys[index] const weaponKey = gridWeapon.weapon_keys[index]
if ( if (
[ [
@ -250,9 +357,9 @@ const WeaponUnit = (props: Props) => {
'chain-glorification', 'chain-glorification',
].includes(weaponKey.slug) ].includes(weaponKey.slug)
) { ) {
filename = `${props.gridWeapon.weapon_keys[index].slug}-${suffix}.png` filename = `${gridWeapon.weapon_keys[index].slug}-${suffix}.png`
} else { } else {
filename = `${props.gridWeapon.weapon_keys[index].slug}.png` filename = `${gridWeapon.weapon_keys[index].slug}.png`
} }
} }
@ -270,12 +377,12 @@ const WeaponUnit = (props: Props) => {
function opusImages() { function opusImages() {
let images: JSX.Element[] = [] let images: JSX.Element[] = []
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.series === 2 && gridWeapon.object.series === 2 &&
props.gridWeapon.weapon_keys && gridWeapon.weapon_keys &&
props.gridWeapon.weapon_keys.length > 0 gridWeapon.weapon_keys.length > 0
) { ) {
for (let i = 0; i < props.gridWeapon.weapon_keys.length; i++) { for (let i = 0; i < gridWeapon.weapon_keys.length; i++) {
const image = opusImage(i) const image = opusImage(i)
if (image) images.push(image) if (image) images.push(image)
} }
@ -288,13 +395,13 @@ const WeaponUnit = (props: Props) => {
const axSkill = getCanonicalAxSkill(index) const axSkill = getCanonicalAxSkill(index)
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.ax && gridWeapon.object.ax &&
props.gridWeapon.object.ax_type > 0 && gridWeapon.object.ax_type > 0 &&
props.gridWeapon.ax && gridWeapon.ax &&
axSkill axSkill
) { ) {
const altText = `${axSkill.name[locale]} Lv${props.gridWeapon.ax[index].strength}` const altText = `${axSkill.name[locale]} Lv${gridWeapon.ax[index].strength}`
return ( return (
<img <img
alt={altText} alt={altText}
@ -309,12 +416,12 @@ const WeaponUnit = (props: Props) => {
function axImages() { function axImages() {
let images: JSX.Element[] = [] let images: JSX.Element[] = []
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.ax && gridWeapon.object.ax &&
props.gridWeapon.ax && gridWeapon.ax &&
props.gridWeapon.ax.length > 0 gridWeapon.ax.length > 0
) { ) {
for (let i = 0; i < props.gridWeapon.ax.length; i++) { for (let i = 0; i < gridWeapon.ax.length; i++) {
const image = axImage(i) const image = axImage(i)
if (image) images.push(image) if (image) images.push(image)
} }
@ -323,46 +430,86 @@ const WeaponUnit = (props: Props) => {
return images return images
} }
function getCanonicalAxSkill(index: number) { // Methods: Layer element rendering
if ( const weaponModal = () => {
props.gridWeapon && if (gridWeapon) {
props.gridWeapon.object.ax &&
props.gridWeapon.object.ax_type > 0 &&
props.gridWeapon.ax
) {
const axOptions = axData[props.gridWeapon.object.ax_type - 1]
const weaponAxSkill: SimpleAxSkill = props.gridWeapon.ax[0]
let axSkill = axOptions.find((ax) => ax.id === weaponAxSkill.modifier)
if (index !== 0 && axSkill && axSkill.secondary) {
const weaponSubAxSkill: SimpleAxSkill = props.gridWeapon.ax[1]
axSkill = axSkill.secondary.find(
(ax) => ax.id === weaponSubAxSkill.modifier
)
}
return axSkill
} else return
}
function passUncapData(index: number) {
if (props.gridWeapon)
props.updateUncap(props.gridWeapon.id, props.position, index)
}
function canBeModified(gridWeapon: GridWeapon) {
const weapon = gridWeapon.object
return ( return (
weapon.ax || <WeaponModal
weapon.awakening || gridWeapon={gridWeapon}
(weapon.series && [2, 3, 17, 22, 24].includes(weapon.series)) open={detailsModalOpen}
onOpenChange={handleWeaponModalOpenChange}
/>
)
}
}
const contextMenu = () => {
if (editable && gridWeapon && gridWeapon.id) {
return (
<>
<ContextMenu onOpenChange={handleContextMenuOpenChange}>
<ContextMenuTrigger asChild>
<Button
accessoryIcon={<SettingsIcon />}
className={buttonClasses}
onClick={handleButtonClicked}
/>
</ContextMenuTrigger>
<ContextMenuContent align="start">
{canBeModified(gridWeapon) ? (
<ContextMenuItem onSelect={openWeaponModal}>
{t('context.modify.weapon')}
</ContextMenuItem>
) : (
''
)}
<ContextMenuItem onSelect={openRemoveWeaponAlert}>
{t('context.remove')}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
{weaponModal()}
{removeAlert()}
</>
)
}
}
const removeAlert = () => {
return (
<Alert
open={alertOpen}
primaryAction={removeWeapon}
primaryActionText={t('modals.weapon.buttons.remove')}
cancelAction={() => setAlertOpen(false)}
cancelActionText={t('buttons.cancel')}
message={
<Trans i18nKey="modals.weapons.messages.remove">
Are you sure you want to remove{' '}
<strong>{{ weapon: gridWeapon?.object.name[locale] }}</strong> from
your team?
</Trans>
}
/>
) )
} }
const searchModal = () => {
return (
<SearchModal
placeholderText={t('search.placeholders.weapon')}
fromPosition={position}
object="weapons"
open={searchModalOpen}
onOpenChange={handleSearchModalOpenChange}
send={updateObject}
/>
)
}
// Methods: Core element rendering
const image = ( const image = (
<div className="WeaponImage"> <div className="WeaponImage" onClick={openSearchModal}>
<div className="Modifiers"> <div className="Modifiers">
{awakeningImage()} {awakeningImage()}
<div className="Skills"> <div className="Skills">
@ -380,7 +527,7 @@ const WeaponUnit = (props: Props) => {
})} })}
src={imageUrl !== '' ? imageUrl : placeholderImageUrl()} src={imageUrl !== '' ? imageUrl : placeholderImageUrl()}
/> />
{props.editable ? ( {editable ? (
<span className="icon"> <span className="icon">
<PlusIcon /> <PlusIcon />
</span> </span>
@ -390,31 +537,12 @@ const WeaponUnit = (props: Props) => {
</div> </div>
) )
const editableImage = (
<SearchModal
placeholderText={t('search.placeholders.weapon')}
fromPosition={props.position}
object="weapons"
send={props.updateObject}
>
{image}
</SearchModal>
)
const unitContent = ( const unitContent = (
<>
<div className={classes}> <div className={classes}>
{props.editable && {contextMenu()}
gridWeapon && {image}
gridWeapon.id && {gridWeapon && weapon ? (
canBeModified(gridWeapon) ? (
<WeaponModal gridWeapon={gridWeapon}>
<Button accessoryIcon={<SettingsIcon />} />
</WeaponModal>
) : (
''
)}
{props.editable ? editableImage : image}
{gridWeapon ? (
<UncapIndicator <UncapIndicator
type="weapon" type="weapon"
ulb={gridWeapon.object.uncap.ulb || false} ulb={gridWeapon.object.uncap.ulb || false}
@ -428,13 +556,15 @@ const WeaponUnit = (props: Props) => {
)} )}
<h3 className="WeaponName">{weapon?.name[locale]}</h3> <h3 className="WeaponName">{weapon?.name[locale]}</h3>
</div> </div>
{searchModal()}
</>
) )
const withHovercard = ( const unitContentWithHovercard = (
<WeaponHovercard gridWeapon={gridWeapon!}>{unitContent}</WeaponHovercard> <WeaponHovercard gridWeapon={gridWeapon!}>{unitContent}</WeaponHovercard>
) )
return gridWeapon && !props.editable ? withHovercard : unitContent return gridWeapon && !editable ? unitContentWithHovercard : unitContent
} }
export default WeaponUnit export default WeaponUnit

View file

@ -6,57 +6,85 @@ export type Awakening = {
ja: string ja: string
} }
} }
export const characterAwakening: Awakening[] = [ export const characterAwakening: ItemSkill[] = [
{ {
id: 0, id: 1,
name: { name: {
en: 'Balanced', en: 'Balanced',
ja: 'バランス', ja: 'バランス',
}, },
}, slug: 'balanced',
{ minValue: 1,
id: 1, maxValue: 9,
name: { fractional: false,
en: 'Attack',
ja: '攻撃',
},
}, },
{ {
id: 2, id: 2,
name: { name: {
en: 'Defense', en: 'Attack',
ja: '防御', ja: '攻撃',
}, },
slug: 'attack',
minValue: 1,
maxValue: 9,
fractional: false,
}, },
{ {
id: 3, id: 3,
name: {
en: 'Defense',
ja: '防御',
},
slug: 'defense',
minValue: 1,
maxValue: 9,
fractional: false,
},
{
id: 4,
name: { name: {
en: 'Multiattack', en: 'Multiattack',
ja: '連続攻撃', ja: '連続攻撃',
}, },
slug: 'multiattack',
minValue: 1,
maxValue: 9,
fractional: false,
}, },
] ]
export const weaponAwakening: Awakening[] = [ export const weaponAwakening: ItemSkill[] = [
{ {
id: 0, id: 1,
name: { name: {
en: 'Attack', en: 'Attack',
ja: '攻撃', ja: '攻撃',
}, },
}, slug: 'attack',
{ minValue: 1,
id: 1, maxValue: 15,
name: { fractional: false,
en: 'Defense',
ja: '防御',
},
}, },
{ {
id: 2, id: 2,
name: {
en: 'Defense',
ja: '防御',
},
slug: 'defense',
minValue: 1,
maxValue: 15,
fractional: false,
},
{
id: 3,
name: { name: {
en: 'Special', en: 'Special',
ja: '特殊', ja: '特殊',
}, },
slug: 'special',
minValue: 1,
maxValue: 15,
fractional: false,
}, },
] ]

View file

@ -1,4 +1,4 @@
export const axData: AxSkill[][] = [ const ax: ItemSkill[][] = [
[ [
{ {
name: { name: {
@ -10,6 +10,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 3.5, maxValue: 3.5,
suffix: '%', suffix: '%',
fractional: true,
secondary: [ secondary: [
{ {
name: { name: {
@ -20,6 +21,7 @@ export const axData: AxSkill[][] = [
slug: 'ca-dmg', slug: 'ca-dmg',
minValue: 2, minValue: 2,
maxValue: 4, maxValue: 4,
fractional: true,
suffix: '%', suffix: '%',
}, },
{ {
@ -31,6 +33,7 @@ export const axData: AxSkill[][] = [
slug: 'da', slug: 'da',
minValue: 1, minValue: 1,
maxValue: 2, maxValue: 2,
fractional: true,
suffix: '%', suffix: '%',
}, },
{ {
@ -42,6 +45,7 @@ export const axData: AxSkill[][] = [
slug: 'ta', slug: 'ta',
minValue: 1, minValue: 1,
maxValue: 2, maxValue: 2,
fractional: true,
suffix: '%', suffix: '%',
}, },
{ {
@ -53,6 +57,7 @@ export const axData: AxSkill[][] = [
slug: 'skill-cap', slug: 'skill-cap',
minValue: 1, minValue: 1,
maxValue: 2, maxValue: 2,
fractional: true,
suffix: '%', suffix: '%',
}, },
], ],
@ -67,6 +72,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 8, maxValue: 8,
suffix: '%', suffix: '%',
fractional: true,
secondary: [ secondary: [
{ {
name: { name: {
@ -77,6 +83,7 @@ export const axData: AxSkill[][] = [
slug: 'hp', slug: 'hp',
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
fractional: true,
suffix: '%', suffix: '%',
}, },
{ {
@ -88,6 +95,7 @@ export const axData: AxSkill[][] = [
slug: 'debuff', slug: 'debuff',
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
fractional: false,
suffix: '%', suffix: '%',
}, },
{ {
@ -99,6 +107,7 @@ export const axData: AxSkill[][] = [
slug: 'healing', slug: 'healing',
minValue: 2, minValue: 2,
maxValue: 5, maxValue: 5,
fractional: true,
suffix: '%', suffix: '%',
}, },
{ {
@ -110,6 +119,7 @@ export const axData: AxSkill[][] = [
slug: 'enmity', slug: 'enmity',
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
fractional: false,
}, },
], ],
}, },
@ -123,6 +133,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 11, maxValue: 11,
suffix: '%', suffix: '%',
fractional: true,
secondary: [ secondary: [
{ {
name: { name: {
@ -133,6 +144,7 @@ export const axData: AxSkill[][] = [
slug: 'def', slug: 'def',
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
fractional: true,
suffix: '%', suffix: '%',
}, },
{ {
@ -144,6 +156,7 @@ export const axData: AxSkill[][] = [
slug: 'debuff', slug: 'debuff',
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
fractional: false,
suffix: '%', suffix: '%',
}, },
{ {
@ -156,6 +169,7 @@ export const axData: AxSkill[][] = [
minValue: 2, minValue: 2,
maxValue: 5, maxValue: 5,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -166,6 +180,7 @@ export const axData: AxSkill[][] = [
slug: 'stamina', slug: 'stamina',
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
fractional: false,
}, },
], ],
}, },
@ -179,6 +194,7 @@ export const axData: AxSkill[][] = [
minValue: 2, minValue: 2,
maxValue: 8.5, maxValue: 8.5,
suffix: '%', suffix: '%',
fractional: true,
secondary: [ secondary: [
{ {
name: { name: {
@ -190,6 +206,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 1.5, maxValue: 1.5,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -201,6 +218,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 5, maxValue: 5,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -212,6 +230,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 2, maxValue: 2,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -222,6 +241,7 @@ export const axData: AxSkill[][] = [
slug: 'stamina', slug: 'stamina',
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
fractional: true,
}, },
], ],
}, },
@ -235,6 +255,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 4, maxValue: 4,
suffix: '%', suffix: '%',
fractional: true,
secondary: [ secondary: [
{ {
name: { name: {
@ -246,6 +267,7 @@ export const axData: AxSkill[][] = [
minValue: 2, minValue: 2,
maxValue: 4, maxValue: 4,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -257,6 +279,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 5, maxValue: 5,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -268,6 +291,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 2, maxValue: 2,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -279,6 +303,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 2, maxValue: 2,
suffix: '%', suffix: '%',
fractional: true,
}, },
], ],
}, },
@ -294,6 +319,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 3.5, maxValue: 3.5,
suffix: '%', suffix: '%',
fractional: true,
secondary: [ secondary: [
{ {
name: { name: {
@ -305,6 +331,7 @@ export const axData: AxSkill[][] = [
minValue: 2, minValue: 2,
maxValue: 8.5, maxValue: 8.5,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -316,6 +343,7 @@ export const axData: AxSkill[][] = [
minValue: 1.5, minValue: 1.5,
maxValue: 4, maxValue: 4,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -327,6 +355,7 @@ export const axData: AxSkill[][] = [
minValue: 0.5, minValue: 0.5,
maxValue: 1.5, maxValue: 1.5,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -337,6 +366,7 @@ export const axData: AxSkill[][] = [
slug: 'skill-supp', slug: 'skill-supp',
minValue: 1, minValue: 1,
maxValue: 5, maxValue: 5,
fractional: false,
}, },
], ],
}, },
@ -350,6 +380,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 8, maxValue: 8,
suffix: '%', suffix: '%',
fractional: true,
secondary: [ secondary: [
{ {
name: { name: {
@ -361,6 +392,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 5, maxValue: 5,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -372,6 +404,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
suffix: '%', suffix: '%',
fractional: false,
}, },
{ {
name: { name: {
@ -383,6 +416,7 @@ export const axData: AxSkill[][] = [
minValue: 2, minValue: 2,
maxValue: 5, maxValue: 5,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -393,6 +427,7 @@ export const axData: AxSkill[][] = [
slug: 'enmity', slug: 'enmity',
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
fractional: false,
}, },
], ],
}, },
@ -406,6 +441,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 11, maxValue: 11,
suffix: '%', suffix: '%',
fractional: true,
secondary: [ secondary: [
{ {
name: { name: {
@ -417,6 +453,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 5, maxValue: 5,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -428,6 +465,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
suffix: '%', suffix: '%',
fractional: false,
}, },
{ {
name: { name: {
@ -439,6 +477,7 @@ export const axData: AxSkill[][] = [
minValue: 2, minValue: 2,
maxValue: 5, maxValue: 5,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -449,6 +488,7 @@ export const axData: AxSkill[][] = [
slug: 'stamina', slug: 'stamina',
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
fractional: false,
}, },
], ],
}, },
@ -462,6 +502,7 @@ export const axData: AxSkill[][] = [
minValue: 2, minValue: 2,
maxValue: 8.5, maxValue: 8.5,
suffix: '%', suffix: '%',
fractional: true,
secondary: [ secondary: [
{ {
name: { name: {
@ -472,6 +513,7 @@ export const axData: AxSkill[][] = [
slug: 'ta', slug: 'ta',
minValue: 1.5, minValue: 1.5,
maxValue: 4, maxValue: 4,
fractional: true,
suffix: '%', suffix: '%',
}, },
{ {
@ -483,6 +525,7 @@ export const axData: AxSkill[][] = [
slug: 'skill-supp', slug: 'skill-supp',
minValue: 1, minValue: 1,
maxValue: 5, maxValue: 5,
fractional: false,
}, },
{ {
name: { name: {
@ -493,6 +536,7 @@ export const axData: AxSkill[][] = [
slug: 'ca-supp', slug: 'ca-supp',
minValue: 1, minValue: 1,
maxValue: 5, maxValue: 5,
fractional: false,
}, },
{ {
name: { name: {
@ -503,6 +547,7 @@ export const axData: AxSkill[][] = [
slug: 'stamina', slug: 'stamina',
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
fractional: false,
}, },
], ],
}, },
@ -516,6 +561,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 4, maxValue: 4,
suffix: '%', suffix: '%',
fractional: true,
secondary: [ secondary: [
{ {
name: { name: {
@ -526,6 +572,7 @@ export const axData: AxSkill[][] = [
slug: 'ca-supp', slug: 'ca-supp',
minValue: 1, minValue: 1,
maxValue: 5, maxValue: 5,
fractional: false,
}, },
{ {
name: { name: {
@ -536,6 +583,7 @@ export const axData: AxSkill[][] = [
slug: 'na-cap', slug: 'na-cap',
minValue: 0.5, minValue: 0.5,
maxValue: 1.5, maxValue: 1.5,
fractional: true,
suffix: '%', suffix: '%',
}, },
{ {
@ -547,6 +595,7 @@ export const axData: AxSkill[][] = [
slug: 'stamina', slug: 'stamina',
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
fractional: false,
}, },
{ {
name: { name: {
@ -557,6 +606,7 @@ export const axData: AxSkill[][] = [
slug: 'enmity', slug: 'enmity',
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
fractional: false,
}, },
], ],
}, },
@ -572,6 +622,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 3.5, maxValue: 3.5,
suffix: '%', suffix: '%',
fractional: true,
secondary: [ secondary: [
{ {
name: { name: {
@ -583,6 +634,7 @@ export const axData: AxSkill[][] = [
minValue: 2, minValue: 2,
maxValue: 4, maxValue: 4,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -605,6 +657,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 2, maxValue: 2,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -616,6 +669,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 2, maxValue: 2,
suffix: '%', suffix: '%',
fractional: true,
}, },
], ],
}, },
@ -629,6 +683,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 8, maxValue: 8,
suffix: '%', suffix: '%',
fractional: true,
secondary: [ secondary: [
{ {
name: { name: {
@ -640,6 +695,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -651,6 +707,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
suffix: '%', suffix: '%',
fractional: false,
}, },
{ {
name: { name: {
@ -662,6 +719,7 @@ export const axData: AxSkill[][] = [
minValue: 2, minValue: 2,
maxValue: 5, maxValue: 5,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -672,6 +730,7 @@ export const axData: AxSkill[][] = [
slug: 'enmity', slug: 'enmity',
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
fractional: false,
}, },
], ],
}, },
@ -685,6 +744,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 11, maxValue: 11,
suffix: '%', suffix: '%',
fractional: true,
secondary: [ secondary: [
{ {
name: { name: {
@ -696,6 +756,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -707,6 +768,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
suffix: '%', suffix: '%',
fractional: false,
}, },
{ {
name: { name: {
@ -718,6 +780,7 @@ export const axData: AxSkill[][] = [
minValue: 2, minValue: 2,
maxValue: 5, maxValue: 5,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -728,6 +791,7 @@ export const axData: AxSkill[][] = [
slug: 'stamina', slug: 'stamina',
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
fractional: false,
}, },
], ],
}, },
@ -741,6 +805,7 @@ export const axData: AxSkill[][] = [
minValue: 2, minValue: 2,
maxValue: 8.5, maxValue: 8.5,
suffix: '%', suffix: '%',
fractional: true,
secondary: [ secondary: [
{ {
name: { name: {
@ -752,6 +817,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 1.5, maxValue: 1.5,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -763,6 +829,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 5, maxValue: 5,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -774,6 +841,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 2, maxValue: 2,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -784,6 +852,7 @@ export const axData: AxSkill[][] = [
slug: 'stamina', slug: 'stamina',
minValue: 1, minValue: 1,
maxValue: 3, maxValue: 3,
fractional: false,
}, },
], ],
}, },
@ -797,6 +866,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 4, maxValue: 4,
suffix: '%', suffix: '%',
fractional: true,
secondary: [ secondary: [
{ {
name: { name: {
@ -808,6 +878,7 @@ export const axData: AxSkill[][] = [
minValue: 2, minValue: 2,
maxValue: 4, maxValue: 4,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -819,6 +890,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 5, maxValue: 5,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -830,6 +902,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 2, maxValue: 2,
suffix: '%', suffix: '%',
fractional: true,
}, },
{ {
name: { name: {
@ -841,6 +914,7 @@ export const axData: AxSkill[][] = [
minValue: 1, minValue: 1,
maxValue: 2, maxValue: 2,
suffix: '%', suffix: '%',
fractional: true,
}, },
], ],
}, },
@ -854,6 +928,7 @@ export const axData: AxSkill[][] = [
minValue: 5, minValue: 5,
maxValue: 10, maxValue: 10,
suffix: '%', suffix: '%',
fractional: false,
}, },
{ {
name: { name: {
@ -865,6 +940,9 @@ export const axData: AxSkill[][] = [
minValue: 10, minValue: 10,
maxValue: 20, maxValue: 20,
suffix: '%', suffix: '%',
fractional: false,
}, },
], ],
] ]
export default ax

View file

@ -1,5 +1,6 @@
export const allElement: TeamElement = { export const allElement: TeamElement = {
id: -1, id: -1,
opposite_id: -1,
name: { name: {
en: 'All', en: 'All',
ja: '全s', ja: '全s',
@ -9,6 +10,7 @@ export const allElement: TeamElement = {
export const elements: TeamElement[] = [ export const elements: TeamElement[] = [
{ {
id: 0, id: 0,
opposite_id: 0,
name: { name: {
en: 'Null', en: 'Null',
ja: '無', ja: '無',
@ -16,6 +18,7 @@ export const elements: TeamElement[] = [
}, },
{ {
id: 1, id: 1,
opposite_id: 4,
name: { name: {
en: 'Wind', en: 'Wind',
ja: '風', ja: '風',
@ -23,6 +26,7 @@ export const elements: TeamElement[] = [
}, },
{ {
id: 2, id: 2,
opposite_id: 1,
name: { name: {
en: 'Fire', en: 'Fire',
ja: '火', ja: '火',
@ -30,6 +34,7 @@ export const elements: TeamElement[] = [
}, },
{ {
id: 3, id: 3,
opposite_id: 2,
name: { name: {
en: 'Water', en: 'Water',
ja: '水', ja: '水',
@ -37,6 +42,7 @@ export const elements: TeamElement[] = [
}, },
{ {
id: 4, id: 4,
opposite_id: 3,
name: { name: {
en: 'Earth', en: 'Earth',
ja: '土', ja: '土',
@ -44,6 +50,7 @@ export const elements: TeamElement[] = [
}, },
{ {
id: 5, id: 5,
opposite_id: 6,
name: { name: {
en: 'Dark', en: 'Dark',
ja: '闇', ja: '闇',
@ -51,6 +58,7 @@ export const elements: TeamElement[] = [
}, },
{ {
id: 6, id: 6,
opposite_id: 5,
name: { name: {
en: 'Light', en: 'Light',
ja: '光', ja: '光',

332
data/overMastery.tsx Normal file
View file

@ -0,0 +1,332 @@
const overMasteryPrimary: ItemSkill[] = [
{
name: {
en: 'ATK',
ja: '攻撃',
},
id: 1,
slug: 'atk',
minValue: 300,
maxValue: 3000,
suffix: '',
fractional: false,
values: [300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000],
},
{
name: {
en: 'HP',
ja: 'HP',
},
id: 2,
slug: 'hp',
minValue: 150,
maxValue: 1500,
suffix: '',
fractional: false,
values: [150, 300, 450, 600, 750, 900, 1050, 1200, 1350, 1500],
},
]
const overMasterySecondary: ItemSkill[] = [
{
name: {
en: 'Debuff Success',
ja: '弱体成功率',
},
id: 3,
slug: 'debuff-success',
minValue: 6,
maxValue: 15,
suffix: '%',
fractional: false,
values: [6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
},
{
name: {
en: 'Skill DMG Cap',
ja: 'アビダメ上限',
},
id: 4,
slug: 'skill-cap',
minValue: 6,
maxValue: 15,
suffix: '%',
fractional: false,
values: [6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
},
{
name: {
en: 'C.A. DMG',
ja: '奥義ダメージ',
},
id: 5,
slug: 'ca-dmg',
minValue: 10,
maxValue: 30,
suffix: '%',
fractional: false,
values: [10, 12, 14, 16, 18, 20, 22, 24, 27, 30],
},
{
name: {
en: 'C.A. DMG Cap',
ja: '奥義ダメージ上限',
},
id: 6,
slug: 'ca-cap',
minValue: 6,
maxValue: 15,
suffix: '%',
fractional: false,
values: [6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
},
{
name: {
en: 'Stamina',
ja: '渾身',
},
id: 7,
slug: 'stamina',
minValue: 1,
maxValue: 10,
suffix: '',
fractional: false,
values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
},
{
name: {
en: 'Enmity',
ja: '背水',
},
id: 8,
slug: 'enmity',
minValue: 1,
maxValue: 10,
suffix: '',
fractional: false,
values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
},
{
name: {
en: 'Critical Hit',
ja: 'クリティカル確率',
},
id: 9,
slug: 'crit',
minValue: 10,
maxValue: 30,
suffix: '%',
fractional: false,
values: [10, 12, 14, 16, 18, 20, 22, 24, 27, 30],
},
]
const overMasteryTertiary: ItemSkill[] = [
{
name: {
en: 'Double Attack',
ja: 'ダブルアタック確率',
},
id: 10,
slug: 'da',
minValue: 6,
maxValue: 15,
suffix: '%',
fractional: false,
values: [6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
},
{
name: {
en: 'Triple Attack',
ja: 'トリプルアタック確率',
},
id: 11,
slug: 'ta',
minValue: 1,
maxValue: 10,
suffix: '%',
fractional: false,
values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
},
{
name: {
en: 'DEF',
ja: '防御',
},
id: 12,
slug: 'def',
minValue: 6,
maxValue: 20,
suffix: '%',
fractional: false,
values: [6, 7, 8, 9, 10, 12, 14, 16, 18, 20],
},
{
name: {
en: 'Healing',
ja: '回復性能',
},
id: 13,
slug: 'heal',
minValue: 3,
maxValue: 30,
suffix: '%',
fractional: false,
values: [3, 6, 9, 12, 15, 18, 21, 24, 27, 30],
},
{
name: {
en: 'Debuff Resistance',
ja: '弱体耐性',
},
id: 14,
slug: 'debuff-resist',
minValue: 6,
maxValue: 15,
suffix: '%',
fractional: false,
values: [6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
},
{
name: {
en: 'Dodge',
ja: '回避',
},
id: 15,
slug: 'dodge',
minValue: 1,
maxValue: 10,
suffix: '%',
fractional: false,
values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
},
]
export const overMastery = {
a: overMasteryPrimary,
b: overMasterySecondary,
c: overMasteryTertiary,
}
export const aetherialMastery: ItemSkill[] = [
{
name: {
en: 'Double Attack',
ja: 'ダブルアタック確率',
},
id: 1,
slug: 'da',
minValue: 10,
maxValue: 17,
suffix: '%',
fractional: false,
},
{
name: {
en: 'Triple Attack',
ja: 'トリプルアタック確率',
},
id: 2,
slug: 'ta',
minValue: 5,
maxValue: 12,
suffix: '%',
fractional: false,
},
{
name: {
en: '{Element} ATK Up',
ja: '{属性}攻撃',
},
id: 3,
slug: 'element-atk',
minValue: 15,
maxValue: 22,
suffix: '%',
fractional: false,
},
{
name: {
en: '{Element} Resistance',
ja: '{属性}軽減',
},
id: 4,
slug: 'element-resist',
minValue: 5,
maxValue: 12,
suffix: '%',
fractional: false,
},
{
name: {
en: 'Stamina',
ja: '渾身',
},
id: 5,
slug: 'stamina',
minValue: 5,
maxValue: 12,
suffix: '',
fractional: false,
},
{
name: {
en: 'Enmity',
ja: '背水',
},
id: 6,
slug: 'enmity',
minValue: 5,
maxValue: 12,
suffix: '',
fractional: false,
},
{
name: {
en: 'Supplemental DMG',
ja: '与ダメ上昇',
},
id: 7,
slug: 'supplemental',
minValue: 5,
maxValue: 12,
suffix: '',
fractional: false,
},
{
name: {
en: 'Critical Hit',
ja: 'クリティカル',
},
id: 8,
slug: 'crit',
minValue: 18,
maxValue: 35,
suffix: '%',
fractional: false,
},
{
name: {
en: 'Counters on Dodge',
ja: 'カウンター(回避)',
},
id: 9,
slug: 'counter-dodge',
minValue: 5,
maxValue: 12,
suffix: '%',
fractional: false,
},
{
name: {
en: 'Counters on DMG',
ja: 'カウンター(被ダメ)',
},
id: 10,
slug: 'counter-dmg',
minValue: 10,
maxValue: 17,
suffix: '%',
fractional: false,
},
]

56
hooks/useLockedBody.tsx Normal file
View file

@ -0,0 +1,56 @@
// https://usehooks-ts.com/react-hook/use-locked-body
import { useEffect, useState } from 'react'
import { useIsomorphicLayoutEffect } from 'usehooks-ts'
type UseLockedBodyOutput = [boolean, (locked: boolean) => void]
function useLockedBody(
initialLocked = false,
rootId = '___gatsby' // Default to `___gatsby` to not introduce breaking change
): UseLockedBodyOutput {
const [locked, setLocked] = useState(initialLocked)
// Do the side effect before render
useIsomorphicLayoutEffect(() => {
if (!locked) {
return
}
// Save initial body style
const originalOverflow = document.body.style.overflow
const originalPaddingRight = document.body.style.paddingRight
// Lock body scroll
document.body.style.overflow = 'hidden'
// Get the scrollBar width
const root = document.getElementById(rootId) // or root
const scrollBarWidth = root ? root.offsetWidth - root.scrollWidth : 0
// Avoid width reflow
if (scrollBarWidth) {
document.body.style.paddingRight = `${scrollBarWidth}px`
}
return () => {
document.body.style.overflow = originalOverflow
if (scrollBarWidth) {
document.body.style.paddingRight = originalPaddingRight
}
}
}, [locked])
// Update state if initialValue changes
useEffect(() => {
if (locked !== initialLocked) {
setLocked(initialLocked)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialLocked])
return [locked, setLocked]
}
export default useLockedBody

20
package-lock.json generated
View file

@ -40,6 +40,7 @@
"react-string-replace": "^1.1.0", "react-string-replace": "^1.1.0",
"sanitize-html": "^2.8.1", "sanitize-html": "^2.8.1",
"sass": "^1.49.0", "sass": "^1.49.0",
"usehooks-ts": "^2.9.1",
"valtio": "^1.3.0", "valtio": "^1.3.0",
"youtube-api-v3-wrapper": "^2.3.0" "youtube-api-v3-wrapper": "^2.3.0"
}, },
@ -7136,6 +7137,19 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/usehooks-ts": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.9.1.tgz",
"integrity": "sha512-2FAuSIGHlY+apM9FVlj8/oNhd+1y+Uwv5QNkMQz1oSfdHk4PXo1qoCw9I5M7j0vpH8CSWFJwXbVPeYDjLCx9PA==",
"engines": {
"node": ">=16.15.0",
"npm": ">=8"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -12203,6 +12217,12 @@
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"requires": {} "requires": {}
}, },
"usehooks-ts": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.9.1.tgz",
"integrity": "sha512-2FAuSIGHlY+apM9FVlj8/oNhd+1y+Uwv5QNkMQz1oSfdHk4PXo1qoCw9I5M7j0vpH8CSWFJwXbVPeYDjLCx9PA==",
"requires": {}
},
"util-deprecate": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View file

@ -45,6 +45,7 @@
"react-string-replace": "^1.1.0", "react-string-replace": "^1.1.0",
"sanitize-html": "^2.8.1", "sanitize-html": "^2.8.1",
"sass": "^1.49.0", "sass": "^1.49.0",
"usehooks-ts": "^2.9.1",
"valtio": "^1.3.0", "valtio": "^1.3.0",
"youtube-api-v3-wrapper": "^2.3.0" "youtube-api-v3-wrapper": "^2.3.0"
}, },

View file

@ -13,7 +13,7 @@ import setUserToken from '~utils/setUserToken'
import extractFilters from '~utils/extractFilters' import extractFilters from '~utils/extractFilters'
import organizeRaids from '~utils/organizeRaids' import organizeRaids from '~utils/organizeRaids'
import useDidMountEffect from '~utils/useDidMountEffect' import useDidMountEffect from '~utils/useDidMountEffect'
import { elements, allElement } from '~utils/Element' import { elements, allElement } from '~data/elements'
import { emptyPaginationObject } from '~utils/emptyStates' import { emptyPaginationObject } from '~utils/emptyStates'
import GridRep from '~components/GridRep' import GridRep from '~components/GridRep'

View file

@ -14,7 +14,7 @@ import setUserToken from '~utils/setUserToken'
import extractFilters from '~utils/extractFilters' import extractFilters from '~utils/extractFilters'
import organizeRaids from '~utils/organizeRaids' import organizeRaids from '~utils/organizeRaids'
import useDidMountEffect from '~utils/useDidMountEffect' import useDidMountEffect from '~utils/useDidMountEffect'
import { elements, allElement } from '~utils/Element' import { elements, allElement } from '~data/elements'
import { emptyPaginationObject } from '~utils/emptyStates' import { emptyPaginationObject } from '~utils/emptyStates'
import GridRep from '~components/GridRep' import GridRep from '~components/GridRep'

View file

@ -14,7 +14,7 @@ import setUserToken from '~utils/setUserToken'
import extractFilters from '~utils/extractFilters' import extractFilters from '~utils/extractFilters'
import organizeRaids from '~utils/organizeRaids' import organizeRaids from '~utils/organizeRaids'
import useDidMountEffect from '~utils/useDidMountEffect' import useDidMountEffect from '~utils/useDidMountEffect'
import { elements, allElement } from '~utils/Element' import { elements, allElement } from '~data/elements'
import { emptyPaginationObject } from '~utils/emptyStates' import { emptyPaginationObject } from '~utils/emptyStates'
import GridRep from '~components/GridRep' import GridRep from '~components/GridRep'

View file

@ -32,6 +32,14 @@
"new": "New", "new": "New",
"wiki": "View more on gbf.wiki" "wiki": "View more on gbf.wiki"
}, },
"context": {
"modify": {
"character": "Modify character",
"summon": "Modify summon",
"weapon": "Modify weapon"
},
"remove": "Remove from grid"
},
"filters": { "filters": {
"labels": { "labels": {
"element": "Element", "element": "Element",
@ -137,6 +145,22 @@
"changelog": { "changelog": {
"title": "Changelog" "title": "Changelog"
}, },
"characters": {
"title": "Character",
"subtitles": {
"ring": "Over Mastery",
"earring": "Aetherial Mastery",
"permanent": "Permanent Mastery",
"awakening": "Awakening"
},
"messages": {
"remove": "Are you sure you want to remove <strong>{{character}}</strong> from your team?"
},
"buttons": {
"confirm": "Save character",
"remove": "Remove character"
}
},
"conflict": { "conflict": {
"character": "Only one <strong>version of a character</strong> can be included in each party. Do you want to change your party members?", "character": "Only one <strong>version of a character</strong> can be included in each party. Do you want to change your party members?",
"weapon": { "weapon": {
@ -229,7 +253,8 @@
"weapon": { "weapon": {
"title": "Modify Weapon", "title": "Modify Weapon",
"buttons": { "buttons": {
"confirm": "Save weapon" "confirm": "Save weapon",
"remove": "Remove weapon"
}, },
"subtitles": { "subtitles": {
"element": "Element", "element": "Element",
@ -339,5 +364,6 @@
"no_title": "Untitled", "no_title": "Untitled",
"no_raid": "No raid", "no_raid": "No raid",
"no_user": "Anonymous", "no_user": "Anonymous",
"no_job": "No class" "no_job": "No class",
"no_value": "No value"
} }

View file

@ -32,6 +32,14 @@
"new": "作成", "new": "作成",
"wiki": "gbf.wikiで詳しく見る" "wiki": "gbf.wikiで詳しく見る"
}, },
"context": {
"modify": {
"character": "キャラクターを変更",
"summon": "召喚石を変更",
"weapon": "武器を変更"
},
"remove": "編成から削除"
},
"filters": { "filters": {
"labels": { "labels": {
"element": "属性", "element": "属性",
@ -137,6 +145,22 @@
"changelog": { "changelog": {
"title": "変更ログ" "title": "変更ログ"
}, },
"characters": {
"title": "キャラクター",
"subtitles": {
"ring": "EXリミットボーナス",
"earring": "エーテリアルプラス",
"permanent": "マスタリーボーナス",
"awakening": "覚醒"
},
"messages": {
"remove": "<strong>{{character}}</strong>を編成から削除しますか?"
},
"buttons": {
"confirm": "キャラクターを変更する",
"remove": "キャラクターを削除する"
}
},
"conflict": { "conflict": {
"character": "<strong>同じ名前のキャラクター</strong>がパーティに編成されています。<br />以下のキャラクターを入れ替えますか?", "character": "<strong>同じ名前のキャラクター</strong>がパーティに編成されています。<br />以下のキャラクターを入れ替えますか?",
"weapon": { "weapon": {
@ -230,7 +254,8 @@
"weapon": { "weapon": {
"title": "武器変更", "title": "武器変更",
"buttons": { "buttons": {
"confirm": "武器を変更する" "confirm": "武器を変更する",
"remove": "武器を削除する"
}, },
"subtitles": { "subtitles": {
"element": "属性", "element": "属性",
@ -340,5 +365,6 @@
"no_title": "無題", "no_title": "無題",
"no_raid": "マルチなし", "no_raid": "マルチなし",
"no_user": "無名", "no_user": "無名",
"no_job": "ジョブなし" "no_job": "ジョブなし",
"no_value": "値なし"
} }

View file

@ -140,7 +140,7 @@ $dialog--bg--dark: $grey-25;
// Color Definitions: Menu // Color Definitions: Menu
$menu--bg--light: $grey-100; $menu--bg--light: $grey-100;
$menu--bg--dark: $grey-05; $menu--bg--dark: $grey-10;
$menu--text--light: $grey-90; $menu--text--light: $grey-90;
$menu--text--dark: $grey-50; $menu--text--dark: $grey-50;
$menu--separator--light: $grey-90; $menu--separator--light: $grey-90;
@ -307,3 +307,7 @@ $item-corner: $unit-half;
// Shadows // Shadows
$hover-stroke: 1px solid rgba(0, 0, 0, 0.1); $hover-stroke: 1px solid rgba(0, 0, 0, 0.1);
$hover-shadow: rgba(0, 0, 0, 0.08) 0px 0px 14px; $hover-shadow: rgba(0, 0, 0, 0.08) 0px 0px 14px;
// Durations
$duration-color-fade: 0.24s;
$duration-zoom: 0.18s;

View file

@ -3,8 +3,11 @@ interface GridCharacter {
position: number position: number
object: Character object: Character
uncap_level: number uncap_level: number
over_mastery: CharacterOverMastery
aetherial_mastery: ExtendedMastery
awakening: { awakening: {
type: number type: number
level: number level: number
} }
perpetuity: boolean
} }

View file

@ -1,4 +1,4 @@
interface AxSkill { interface ItemSkill {
name: { name: {
[key: string]: string [key: string]: string
en: string en: string
@ -8,6 +8,8 @@ interface AxSkill {
slug: string slug: string
minValue: number minValue: number
maxValue: number maxValue: number
fractional: boolean
suffix?: string suffix?: string
secondary?: AxSkill[] secondary?: ItemSkill[]
values?: number[]
} }

View file

@ -1,5 +1,6 @@
interface TeamElement { interface TeamElement {
id: number id: number
opposite_id: number
name: { name: {
en: string en: string
ja: string ja: string

13
types/index.d.ts vendored
View file

@ -35,3 +35,16 @@ export type DetailsObject = {
job?: Job job?: Job
extra?: boolean extra?: boolean
} }
export type ExtendedMastery = {
modifier?: number
strength?: number
}
export type CharacterOverMastery = {
[key: number]: ExtendedMastery
1: ExtendedMastery
2: ExtendedMastery
3: ExtendedMastery
4: ExtendedMastery
}

View file

@ -152,6 +152,7 @@ class Api {
const api: Api = new Api({ url: process.env.NEXT_PUBLIC_SIERO_API_URL || 'https://localhost:3000/api/v1'}) const api: Api = new Api({ url: process.env.NEXT_PUBLIC_SIERO_API_URL || 'https://localhost:3000/api/v1'})
api.createEntity({ name: 'users' }) api.createEntity({ name: 'users' })
api.createEntity({ name: 'parties' }) api.createEntity({ name: 'parties' })
api.createEntity({ name: 'grid_characters' })
api.createEntity({ name: 'grid_weapons' }) api.createEntity({ name: 'grid_weapons' })
api.createEntity({ name: 'characters' }) api.createEntity({ name: 'characters' })
api.createEntity({ name: 'weapons' }) api.createEntity({ name: 'weapons' })

View file

@ -0,0 +1,54 @@
import { elements } from '~data/elements'
import { aetherialMastery } from '~data/overMastery'
export default function elementalizeAetherialMastery(
gridCharacter: GridCharacter
) {
const elementalized = aetherialMastery.map((modifier) => {
const element = elements.find((a) => a.id === gridCharacter.object.element)
const oppositeElement = elements.find((b) => {
if (element) return b.id === element.opposite_id
})
const newModifier = modifier
if (element && oppositeElement && modifier.name.en.includes('{Element}')) {
if (modifier.id === 3) {
newModifier.name.en = newModifier.name.en.replace(
'{Element}',
element.name.en
)
newModifier.name.ja = newModifier.name.ja.replace(
'{属性}',
`${element.name.ja}属性`
)
} else if (modifier.id === 4) {
newModifier.name.en = newModifier.name.en.replace(
'{Element}',
oppositeElement.name.en
)
newModifier.name.ja = newModifier.name.ja.replace(
'{属性}',
`${oppositeElement.name.ja}属性`
)
}
}
return newModifier
})
elementalized.unshift({
id: 0,
name: {
en: 'No aetherial mastery',
ja: 'エーテリアルプラス',
},
slug: 'no-mastery',
minValue: 0,
maxValue: 0,
fractional: false,
})
return elementalized
}

View file

@ -1,4 +1,4 @@
import { elements, allElement } from '~utils/Element' import { elements, allElement } from '~data/elements'
export default (query: { [index: string]: string }, raids: Raid[]) => { export default (query: { [index: string]: string }, raids: Raid[]) => {
// Extract recency filter // Extract recency filter

View file

@ -1,4 +1,4 @@
import { weaponKeyGroups } from './weaponKeyGroups' import { weaponKeyGroups } from '../data/weaponKeyGroups'
export type GroupedWeaponKeys = { export type GroupedWeaponKeys = {
[key: string]: WeaponKey[] [key: string]: WeaponKey[]

View file

@ -1,4 +1,4 @@
import { weaponSeries } from '~utils/weaponSeries' import { weaponSeries } from '~data/weaponSeries'
export default (id: number) => export default (id: number) =>
weaponSeries.find((series) => series.id === id)?.slug weaponSeries.find((series) => series.id === id)?.slug