Merge pull request #24 from jedmund/i18n

Add internationalization support
This commit is contained in:
Justin Edmund 2022-03-06 02:16:01 -08:00 committed by GitHub
commit b57e81ddf4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
86 changed files with 2475 additions and 447 deletions

View file

@ -1,21 +1,24 @@
import React from 'react' import React from 'react'
import { useTranslation } from 'next-i18next'
import * as Dialog from '@radix-ui/react-dialog' import * as Dialog from '@radix-ui/react-dialog'
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from '~public/icons/Cross.svg'
import './index.scss' import './index.scss'
const AboutModal = () => { const AboutModal = () => {
const { t } = useTranslation('common')
return ( return (
<Dialog.Root> <Dialog.Root>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>
<li className="MenuItem"> <li className="MenuItem">
<span>About</span> <span>{t('modals.about.title')}</span>
</li> </li>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Content className="About Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }> <Dialog.Content className="About Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }>
<div className="DialogHeader"> <div className="DialogHeader">
<Dialog.Title className="DialogTitle">About</Dialog.Title> <Dialog.Title className="DialogTitle">{t('menu.about')}</Dialog.Title>
<Dialog.Close className="DialogClose" asChild> <Dialog.Close className="DialogClose" asChild>
<span> <span>
<CrossIcon /> <CrossIcon />

View file

@ -98,6 +98,10 @@
font-size: $font-small; font-size: $font-small;
line-height: 1.1; line-height: 1.1;
max-width: 300px; max-width: 300px;
&.jp {
max-width: 270px;
}
} }
} }

View file

@ -1,6 +1,8 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useCookies } from 'react-cookie' import { useCookies } from 'react-cookie'
import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next'
import * as Dialog from '@radix-ui/react-dialog' import * as Dialog from '@radix-ui/react-dialog'
import * as Switch from '@radix-ui/react-switch' import * as Switch from '@radix-ui/react-switch'
@ -17,13 +19,16 @@ import './index.scss'
const AccountModal = () => { const AccountModal = () => {
const { account } = useSnapshot(accountState) const { account } = useSnapshot(accountState)
// Cookies const router = useRouter()
const [accountCookies] = useCookies(['account']) const { t } = useTranslation('common')
const [userCookies, setUserCookies] = useCookies(['user']) const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const headers = (accountCookies.account != null) ? { // Cookies
const [cookies, setCookies] = useCookies()
const headers = (cookies.account != null) ? {
headers: { headers: {
'Authorization': `Bearer ${accountCookies.account.access_token}` 'Authorization': `Bearer ${cookies.account.access_token}`
} }
} : {} } : {}
@ -39,14 +44,15 @@ const AccountModal = () => {
const privateSelect = React.createRef<HTMLInputElement>() const privateSelect = React.createRef<HTMLInputElement>()
useEffect(() => { useEffect(() => {
if (userCookies.user) setPicture(userCookies.user.picture) console.log(cookies.user)
if (userCookies.user) setLanguage(userCookies.user.language) if (cookies.user) setPicture(cookies.user.picture)
}, [userCookies]) if (cookies.user) setLanguage(cookies.user.language)
}, [cookies])
const pictureOptions = ( const pictureOptions = (
pictureData.sort((a, b) => (a.name.en > b.name.en) ? 1 : -1).map((item, i) => { pictureData.sort((a, b) => (a.name.en > b.name.en) ? 1 : -1).map((item, i) => {
return ( return (
<option key={`picture-${i}`} value={item.filename}>{item.name.en}</option> <option key={`picture-${i}`} value={item.filename}>{item.name[locale]}</option>
) )
}) })
) )
@ -77,7 +83,7 @@ const AccountModal = () => {
} }
} }
api.endpoints.users.update(accountCookies.account.user_id, object, headers) api.endpoints.users.update(cookies.account.user_id, object, headers)
.then(response => { .then(response => {
const user = response.data.user const user = response.data.user
@ -87,9 +93,8 @@ const AccountModal = () => {
language: user.language, language: user.language,
} }
setUserCookies('user', cookieObj, { path: '/'}) setCookies('user', cookieObj, { path: '/'})
accountState.account.language = user.language
accountState.account.user = { accountState.account.user = {
id: user.id, id: user.id,
username: user.username, username: user.username,
@ -98,9 +103,17 @@ const AccountModal = () => {
} }
setOpen(false) setOpen(false)
changeLanguage(user.language)
}) })
} }
function changeLanguage(newLanguage: string) {
if (newLanguage !== router.locale) {
setCookies('NEXT_LOCALE', newLanguage, { path: '/'})
router.push(router.asPath, undefined, { locale: newLanguage })
}
}
function openChange(open: boolean) { function openChange(open: boolean) {
setOpen(open) setOpen(open)
} }
@ -109,14 +122,14 @@ const AccountModal = () => {
<Dialog.Root open={open} onOpenChange={openChange}> <Dialog.Root open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>
<li className="MenuItem"> <li className="MenuItem">
<span>Settings</span> <span>{t('menu.settings')}</span>
</li> </li>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Content className="Account Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }> <Dialog.Content className="Account Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }>
<div className="DialogHeader"> <div className="DialogHeader">
<div className="DialogTop"> <div className="DialogTop">
<Dialog.Title className="SubTitle">Account Settings</Dialog.Title> <Dialog.Title className="SubTitle">{t('modals.settings.title')}</Dialog.Title>
<Dialog.Title className="DialogTitle">@{account.user?.username}</Dialog.Title> <Dialog.Title className="DialogTitle">@{account.user?.username}</Dialog.Title>
</div> </div>
<Dialog.Close className="DialogClose" asChild> <Dialog.Close className="DialogClose" asChild>
@ -129,7 +142,7 @@ const AccountModal = () => {
<form onSubmit={update}> <form onSubmit={update}>
<div className="field"> <div className="field">
<div className="left"> <div className="left">
<label>Picture</label> <label>{t('modals.settings.labels.picture')}</label>
</div> </div>
<div className={`preview ${pictureData.find(i => i.filename === picture)?.element}`}> <div className={`preview ${pictureData.find(i => i.filename === picture)?.element}`}>
@ -147,18 +160,18 @@ const AccountModal = () => {
</div> </div>
<div className="field"> <div className="field">
<div className="left"> <div className="left">
<label>Language</label> <label>{t('modals.settings.labels.language')}</label>
</div> </div>
<select name="language" onChange={handleLanguageChange} value={language} ref={languageSelect}> <select name="language" onChange={handleLanguageChange} value={language} ref={languageSelect}>
<option key="en" value="en">English</option> <option key="en" value="en">{t('modals.settings.language.english')}</option>
<option key="jp" value="jp">Japanese</option> <option key="jp" value="ja">{t('modals.settings.language.japanese')}</option>
</select> </select>
</div> </div>
<div className="field"> <div className="field">
<div className="left"> <div className="left">
<label>Private</label> <label>{t('modals.settings.labels.private')}</label>
<p>Hide your profile and prevent your grids from showing up in collections</p> <p className={locale}>{t('modals.settings.descriptions.private')}</p>
</div> </div>
<Switch.Root className="Switch" onCheckedChange={handlePrivateChange} checked={privateProfile}> <Switch.Root className="Switch" onCheckedChange={handlePrivateChange} checked={privateProfile}>
@ -166,7 +179,7 @@ const AccountModal = () => {
</Switch.Root> </Switch.Root>
</div> </div>
<Button>Save settings</Button> <Button>{t('modals.settings.buttons.confirm')}</Button>
</form> </form>
</Dialog.Content> </Dialog.Content>
<Dialog.Overlay className="Overlay" /> <Dialog.Overlay className="Overlay" />

View file

@ -1,4 +1,7 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import classNames from 'classnames' import classNames from 'classnames'
import { axData } from '~utils/axData' import { axData } from '~utils/axData'
@ -19,6 +22,10 @@ interface Props {
} }
const AXSelect = (props: Props) => { const AXSelect = (props: Props) => {
const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const { t } = useTranslation('common')
// Set up form states and error handling // Set up form states and error handling
const [errors, setErrors] = useState<ErrorMap>({ const [errors, setErrors] = useState<ErrorMap>({
axValue1: '', axValue1: '',
@ -84,7 +91,7 @@ const AXSelect = (props: Props) => {
if (modifierSet == 0) { if (modifierSet == 0) {
axOptionElements = axOptions.map((ax, i) => { axOptionElements = axOptions.map((ax, i) => {
return ( return (
<option key={i} value={ax.id}>{ax.name.en}</option> <option key={i} value={ax.id}>{ax.name[locale]}</option>
) )
}) })
} else { } else {
@ -103,14 +110,14 @@ const AXSelect = (props: Props) => {
const secondaryAxOptions = primarySkill.secondary const secondaryAxOptions = primarySkill.secondary
axOptionElements = secondaryAxOptions.map((ax, i) => { axOptionElements = secondaryAxOptions.map((ax, i) => {
return ( return (
<option key={i} value={ax.id}>{ax.name.en}</option> <option key={i} value={ax.id}>{ax.name[locale]}</option>
) )
}) })
} }
} }
} }
axOptionElements?.unshift(<option key={-1} value={-1}>No AX Skill</option>) axOptionElements?.unshift(<option key={-1} value={-1}>{t('ax.no_skill')}</option>)
return axOptionElements return axOptionElements
} }
@ -156,11 +163,19 @@ const AXSelect = (props: Props) => {
let newErrors = {...errors} let newErrors = {...errors}
if (value < primaryAxSkill.minValue) { if (value < primaryAxSkill.minValue) {
newErrors.axValue1 = `${primaryAxSkill.name.en} must be at least ${primaryAxSkill.minValue}${ (primaryAxSkill.suffix) ? primaryAxSkill.suffix : ''}` newErrors.axValue1 = t('ax.errors.value_too_low', {
name: primaryAxSkill.name[locale],
minValue: primaryAxSkill.minValue,
suffix: (primaryAxSkill.suffix) ? primaryAxSkill.suffix : ''
})
} else if (value > primaryAxSkill.maxValue) { } else if (value > primaryAxSkill.maxValue) {
newErrors.axValue1 = `${primaryAxSkill.name.en} cannot be greater than ${primaryAxSkill.maxValue}${ (primaryAxSkill.suffix) ? primaryAxSkill.suffix : ''}` newErrors.axValue1 = t('ax.errors.value_too_high', {
name: primaryAxSkill.name[locale],
maxValue: primaryAxSkill.minValue,
suffix: (primaryAxSkill.suffix) ? primaryAxSkill.suffix : ''
})
} else if (!value || value <= 0) { } else if (!value || value <= 0) {
newErrors.axValue1 = `${primaryAxSkill.name.en} must have a value` newErrors.axValue1 = t('ax.errors.value_empty', { name: primaryAxSkill.name[locale] })
} else { } else {
newErrors.axValue1 = '' newErrors.axValue1 = ''
} }
@ -179,13 +194,21 @@ const AXSelect = (props: Props) => {
if (secondaryAxSkill) { if (secondaryAxSkill) {
if (value < secondaryAxSkill.minValue) { if (value < secondaryAxSkill.minValue) {
newErrors.axValue2 = `${secondaryAxSkill.name.en} must be at least ${secondaryAxSkill.minValue}${ (secondaryAxSkill.suffix) ? secondaryAxSkill.suffix : ''}` newErrors.axValue2 = t('ax.errors.value_too_low', {
name: secondaryAxSkill.name[locale],
minValue: secondaryAxSkill.minValue,
suffix: (secondaryAxSkill.suffix) ? secondaryAxSkill.suffix : ''
})
} else if (value > secondaryAxSkill.maxValue) { } else if (value > secondaryAxSkill.maxValue) {
newErrors.axValue2 = `${secondaryAxSkill.name.en} cannot be greater than ${secondaryAxSkill.maxValue}${ (secondaryAxSkill.suffix) ? secondaryAxSkill.suffix : ''}` newErrors.axValue2 = t('ax.errors.value_too_high', {
name: secondaryAxSkill.name[locale],
maxValue: secondaryAxSkill.minValue,
suffix: (secondaryAxSkill.suffix) ? secondaryAxSkill.suffix : ''
})
} else if (!secondaryAxSkill.suffix && value % 1 !== 0) { } else if (!secondaryAxSkill.suffix && value % 1 !== 0) {
newErrors.axValue2 = `${secondaryAxSkill.name.en} must be a whole number` newErrors.axValue2 = t('ax.errors.value_not_whole', { name: secondaryAxSkill.name[locale] })
} else if (primaryAxValue <= 0) { } else if (primaryAxValue <= 0) {
newErrors.axValue1 = `${primaryAxSkill.name.en} must have a value` newErrors.axValue1 = t('ax.errors.value_empty', { name: primaryAxSkill.name[locale] })
} else { } else {
newErrors.axValue2 = '' newErrors.axValue2 = ''
} }
@ -224,7 +247,7 @@ const AXSelect = (props: Props) => {
<div className="AXSet"> <div className="AXSet">
<div className="fields"> <div className="fields">
<select key="ax1" defaultValue={ (props.currentSkills && props.currentSkills[0]) ? props.currentSkills[0].modifier : -1 } onChange={handleSelectChange} ref={primaryAxModifierSelect}>{ generateOptions(0) }</select> <select key="ax1" defaultValue={ (props.currentSkills && props.currentSkills[0]) ? props.currentSkills[0].modifier : -1 } onChange={handleSelectChange} ref={primaryAxModifierSelect}>{ generateOptions(0) }</select>
<input defaultValue={ (props.currentSkills && props.currentSkills[0]) ? props.currentSkills[0].strength : 0 } className="Input" type="number" onChange={handleInputChange} ref={primaryAxValueInput} disabled /> <input defaultValue={ (props.currentSkills && props.currentSkills[0]) ? props.currentSkills[0].strength : 0 } className="Input" type="number" onChange={handleInputChange} ref={primaryAxValueInput} disabled={primaryAxValue != 0} />
</div> </div>
<p className={primaryErrorClasses}>{errors.axValue1}</p> <p className={primaryErrorClasses}>{errors.axValue1}</p>
</div> </div>
@ -232,7 +255,7 @@ const AXSelect = (props: Props) => {
<div className={secondarySetClasses}> <div className={secondarySetClasses}>
<div className="fields"> <div className="fields">
<select key="ax2" defaultValue={ (props.currentSkills && props.currentSkills[1]) ? props.currentSkills[1].modifier : -1 } onChange={handleSelectChange} ref={secondaryAxModifierSelect}>{ generateOptions(1) }</select> <select key="ax2" defaultValue={ (props.currentSkills && props.currentSkills[1]) ? props.currentSkills[1].modifier : -1 } onChange={handleSelectChange} ref={secondaryAxModifierSelect}>{ generateOptions(1) }</select>
<input defaultValue={ (props.currentSkills && props.currentSkills[1]) ? props.currentSkills[1].strength : 0 } className="Input" type="number" onChange={handleInputChange} ref={secondaryAxValueInput} disabled /> <input defaultValue={ (props.currentSkills && props.currentSkills[1]) ? props.currentSkills[1].strength : 0 } className="Input" type="number" onChange={handleInputChange} ref={secondaryAxValueInput} disabled={secondaryAxValue != 0} />
</div> </div>
<p className={secondaryErrorClasses}>{errors.axValue2}</p> <p className={secondaryErrorClasses}>{errors.axValue2}</p>
</div> </div>

View file

@ -2,6 +2,8 @@ import React from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useCookies } from 'react-cookie' import { useCookies } from 'react-cookie'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next'
import clonedeep from 'lodash.clonedeep' import clonedeep from 'lodash.clonedeep'
import * as Scroll from 'react-scroll' import * as Scroll from 'react-scroll'
@ -11,15 +13,13 @@ import Header from '~components/Header'
import Button from '~components/Button' import Button from '~components/Button'
import api from '~utils/api' import api from '~utils/api'
import { accountState } from '~utils/accountState'
import { appState, initialAppState } from '~utils/appState' import { appState, initialAppState } from '~utils/appState'
import { ButtonType } from '~utils/enums'
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from '~public/icons/Cross.svg'
import { route } from 'next/dist/server/router'
const BottomHeader = () => { const BottomHeader = () => {
const account = useSnapshot(accountState) const { t } = useTranslation('common')
const app = useSnapshot(appState) const app = useSnapshot(appState)
const router = useRouter() const router = useRouter()
@ -67,9 +67,9 @@ const BottomHeader = () => {
const leftNav = () => { const leftNav = () => {
if (router.pathname === '/p/[party]' || router.pathname === '/new') { if (router.pathname === '/p/[party]' || router.pathname === '/new') {
if (app.party.detailsVisible) { if (app.party.detailsVisible) {
return (<Button icon="edit" active={true} onClick={toggleDetails}>Hide info</Button>) return (<Button icon="edit" active={true} onClick={toggleDetails}>{t('buttons.hide_info')}</Button>)
} else { } else {
return (<Button icon="edit" onClick={toggleDetails}>Edit info</Button>) return (<Button icon="edit" onClick={toggleDetails}>{t('buttons.show_info')}</Button>)
} }
} else { } else {
return (<div />) return (<div />)
@ -84,20 +84,20 @@ const BottomHeader = () => {
<span className='icon'> <span className='icon'>
<CrossIcon /> <CrossIcon />
</span> </span>
<span className="text">Delete team</span> <span className="text">{t('buttons.delete')}</span>
</AlertDialog.Trigger> </AlertDialog.Trigger>
<AlertDialog.Portal> <AlertDialog.Portal>
<AlertDialog.Overlay className="Overlay" /> <AlertDialog.Overlay className="Overlay" />
<AlertDialog.Content className="Dialog"> <AlertDialog.Content className="Dialog">
<AlertDialog.Title className="DialogTitle"> <AlertDialog.Title className="DialogTitle">
Delete team {t('modals.delete_team.title')}
</AlertDialog.Title> </AlertDialog.Title>
<AlertDialog.Description className="DialogDescription"> <AlertDialog.Description className="DialogDescription">
Are you sure you want to permanently delete this team? {t('modals.delete_team.description')}
</AlertDialog.Description> </AlertDialog.Description>
<div className="actions"> <div className="actions">
<AlertDialog.Cancel className="Button modal">Nevermind</AlertDialog.Cancel> <AlertDialog.Cancel className="Button modal">{t('modals.delete_team.buttons.cancel')}</AlertDialog.Cancel>
<AlertDialog.Action className="Button modal destructive" onClick={(e) => deleteTeam(e)}>Yes, delete</AlertDialog.Action> <AlertDialog.Action className="Button modal destructive" onClick={(e) => deleteTeam(e)}>{t('modals.delete_team.buttons.confirm')}</AlertDialog.Action>
</div> </div>
</AlertDialog.Content> </AlertDialog.Content>
</AlertDialog.Portal> </AlertDialog.Portal>

View file

@ -1,11 +1,12 @@
import React from 'react' import React from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import * as HoverCard from '@radix-ui/react-hover-card' 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 './index.scss' import './index.scss'
interface Props { interface Props {
@ -21,6 +22,10 @@ interface KeyNames {
} }
const CharacterHovercard = (props: Props) => { const CharacterHovercard = (props: Props) => {
const router = useRouter()
const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light'] const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana'] const Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana']
@ -56,8 +61,8 @@ const CharacterHovercard = (props: Props) => {
<HoverCard.Content className="Weapon Hovercard"> <HoverCard.Content className="Weapon Hovercard">
<div className="top"> <div className="top">
<div className="title"> <div className="title">
<h4>{ props.gridCharacter.object.name.en }</h4> <h4>{ props.gridCharacter.object.name[locale] }</h4>
<img alt={props.gridCharacter.object.name.en} src={characterImage()} /> <img alt={props.gridCharacter.object.name[locale]} src={characterImage()} />
</div> </div>
<div className="subInfo"> <div className="subInfo">
<div className="icons"> <div className="icons">
@ -76,7 +81,7 @@ const CharacterHovercard = (props: Props) => {
</div> </div>
</div> </div>
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">View more on gbf.wiki</a> <a className={`Button ${tintElement}`} href={wikiUrl} target="_new">{t('buttons.wiki')}</a>
<HoverCard.Arrow /> <HoverCard.Arrow />
</HoverCard.Content> </HoverCard.Content>
</HoverCard.Root> </HoverCard.Root>

View file

@ -1,4 +1,6 @@
import React from 'react' import React from 'react'
import { useRouter } from 'next/router'
import UncapIndicator from '~components/UncapIndicator' import UncapIndicator from '~components/UncapIndicator'
import WeaponLabelIcon from '~components/WeaponLabelIcon' import WeaponLabelIcon from '~components/WeaponLabelIcon'
@ -11,28 +13,29 @@ interface Props {
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light'] const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
class CharacterResult extends React.Component<Props> { const CharacterResult = (props: Props) => {
render() { const router = useRouter()
const character = this.props.data const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
return ( const character = props.data
<li className="CharacterResult" onClick={this.props.onClick}>
<img alt={character.name.en} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01.jpg`} /> return(
<div className="Info"> <li className="CharacterResult" onClick={props.onClick}>
<h5>{character.name.en}</h5> <img alt={character.name[locale]} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01.jpg`} />
<UncapIndicator <div className="Info">
type="character" <h5>{character.name[locale]}</h5>
flb={character.uncap.flb} <UncapIndicator
ulb={character.uncap.ulb} type="character"
special={character.special} flb={character.uncap.flb}
/> ulb={character.uncap.ulb}
<div className="tags"> special={character.special}
<WeaponLabelIcon labelType={Element[character.element]} /> />
</div> <div className="tags">
<WeaponLabelIcon labelType={Element[character.element]} />
</div> </div>
</li> </div>
) </li>
} )
} }
export default CharacterResult export default CharacterResult

View file

@ -1,4 +1,6 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import classnames from 'classnames' import classnames from 'classnames'
import SearchModal from '~components/SearchModal' import SearchModal from '~components/SearchModal'
@ -18,6 +20,11 @@ interface Props {
} }
const CharacterUnit = (props: Props) => { const CharacterUnit = (props: Props) => {
const { t } = useTranslation('common')
const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const [imageUrl, setImageUrl] = useState('') const [imageUrl, setImageUrl] = useState('')
const classes = classnames({ const classes = classnames({
@ -68,7 +75,7 @@ const CharacterUnit = (props: Props) => {
const editableImage = ( const editableImage = (
<SearchModal <SearchModal
placeholderText="Search for a character..." placeholderText={t('search.placeholders.character')}
fromPosition={props.position} fromPosition={props.position}
object="characters" object="characters"
send={props.updateObject}> send={props.updateObject}>
@ -88,7 +95,7 @@ const CharacterUnit = (props: Props) => {
updateUncap={passUncapData} updateUncap={passUncapData}
special={character.special} special={character.special}
/> : '' } /> : '' }
<h3 className="CharacterName">{character?.name.en}</h3> <h3 className="CharacterName">{character?.name[locale]}</h3>
</div> </div>
) )

View file

@ -17,6 +17,11 @@
font-size: $font-regular; font-size: $font-regular;
padding: ($unit) $unit * 2; padding: ($unit) $unit * 2;
&.ja {
padding-top: 6px;
padding-bottom: 10px;
}
&:hover { &:hover {
cursor: pointer; cursor: pointer;
} }

View file

@ -1,4 +1,7 @@
import React from 'react' import React from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import * as ToggleGroup from '@radix-ui/react-toggle-group' import * as ToggleGroup from '@radix-ui/react-toggle-group'
import './index.scss' import './index.scss'
@ -9,28 +12,32 @@ interface Props {
} }
const ElementToggle = (props: Props) => { const ElementToggle = (props: Props) => {
const router = useRouter()
const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
return ( return (
<ToggleGroup.Root className="ToggleGroup" type="single" defaultValue={`${props.currentElement}`} aria-label="Element" onValueChange={props.sendValue}> <ToggleGroup.Root className="ToggleGroup" type="single" defaultValue={`${props.currentElement}`} aria-label="Element" onValueChange={props.sendValue}>
<ToggleGroup.Item className="ToggleItem" value="0" aria-label="null"> <ToggleGroup.Item className={`ToggleItem ${locale}`} value="0" aria-label="null">
Null {t('elements.null')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item className="ToggleItem wind" value="1" aria-label="wind"> <ToggleGroup.Item className={`ToggleItem wind ${locale}`} value="1" aria-label="wind">
Wind {t('elements.wind')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item className="ToggleItem fire" value="2" aria-label="fire"> <ToggleGroup.Item className={`ToggleItem fire ${locale}`} value="2" aria-label="fire">
Fire {t('elements.fire')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item className="ToggleItem water" value="3" aria-label="water"> <ToggleGroup.Item className={`ToggleItem water ${locale}`} value="3" aria-label="water">
Water {t('elements.water')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item className="ToggleItem earth" value="4" aria-label="earth"> <ToggleGroup.Item className={`ToggleItem earth ${locale}`} value="4" aria-label="earth">
Earth {t('elements.earth')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item className="ToggleItem dark" value="5" aria-label="dark"> <ToggleGroup.Item className={`ToggleItem dark ${locale}`} value="5" aria-label="dark">
Dark {t('elements.dark')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item className="ToggleItem light" value="6" aria-label="light"> <ToggleGroup.Item className={`ToggleItem light ${locale}`} value="6" aria-label="light">
Light {t('elements.light')}
</ToggleGroup.Item> </ToggleGroup.Item>
</ToggleGroup.Root> </ToggleGroup.Root>
) )

View file

@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import { useTranslation } from 'next-i18next'
import SummonUnit from '~components/SummonUnit' import SummonUnit from '~components/SummonUnit'
import './index.scss' import './index.scss'
@ -16,9 +17,11 @@ interface Props {
const ExtraSummons = (props: Props) => { const ExtraSummons = (props: Props) => {
const numSummons: number = 2 const numSummons: number = 2
const { t } = useTranslation('common')
return ( return (
<div id="ExtraSummons"> <div id="ExtraSummons">
<span>Sub Aura Summons</span> <span>{t('summons.subaura')}</span>
<ul id="grid_summons"> <ul id="grid_summons">
{ {
Array.from(Array(numSummons)).map((x, i) => { Array.from(Array(numSummons)).map((x, i) => {

View file

@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import { useTranslation } from 'next-i18next'
import WeaponUnit from '~components/WeaponUnit' import WeaponUnit from '~components/WeaponUnit'
import './index.scss' import './index.scss'
@ -15,10 +16,11 @@ interface Props {
const ExtraWeapons = (props: Props) => { const ExtraWeapons = (props: Props) => {
const numWeapons: number = 3 const numWeapons: number = 3
const { t } = useTranslation('common')
return ( return (
<div id="ExtraGrid"> <div id="ExtraGrid">
<span>Additional<br />Weapons</span> <span>{t('extra_weapons')}</span>
<ul className="grid_weapons"> <ul className="grid_weapons">
{ {
Array.from(Array(numWeapons)).map((x, i) => { Array.from(Array(numWeapons)).map((x, i) => {

View file

@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import { useTranslation } from 'next-i18next'
import classNames from 'classnames' import classNames from 'classnames'
import RaidDropdown from '~components/RaidDropdown' import RaidDropdown from '~components/RaidDropdown'
@ -12,6 +13,8 @@ interface Props {
} }
const FilterBar = (props: Props) => { const FilterBar = (props: Props) => {
const { t } = useTranslation('common')
const elementSelect = React.createRef<HTMLSelectElement>() const elementSelect = React.createRef<HTMLSelectElement>()
const raidSelect = React.createRef<HTMLSelectElement>() const raidSelect = React.createRef<HTMLSelectElement>()
const recencySelect = React.createRef<HTMLSelectElement>() const recencySelect = React.createRef<HTMLSelectElement>()
@ -33,14 +36,14 @@ const FilterBar = (props: Props) => {
<div className={classes}> <div className={classes}>
{props.children} {props.children}
<select onChange={selectChanged} ref={elementSelect}> <select onChange={selectChanged} ref={elementSelect}>
<option key={-1} value={-1}>All elements</option> <option key={-1} value={-1}>{t('elements.full.all')}</option>
<option key={-0} value={0}>Null</option> <option key={-0} value={0}>{t('elements.full.null')}</option>
<option key={1}value={1}>Wind</option> <option key={1}value={1}>{t('elements.full.wind')}</option>
<option key={2}value={2}>Fire</option> <option key={2}value={2}>{t('elements.full.fire')}</option>
<option key={3}value={3}>Water</option> <option key={3}value={3}>{t('elements.full.water')}</option>
<option key={4}value={4}>Earth</option> <option key={4}value={4}>{t('elements.full.earth')}</option>
<option key={5}value={5}>Dark</option> <option key={5}value={5}>{t('elements.full.dark')}</option>
<option key={6}value={6}>Light</option> <option key={6}value={6}>{t('elements.full.light')}</option>
</select> </select>
<RaidDropdown <RaidDropdown
allOption={true} allOption={true}
@ -48,13 +51,13 @@ const FilterBar = (props: Props) => {
ref={raidSelect} ref={raidSelect}
/> />
<select onChange={selectChanged} ref={recencySelect}> <select onChange={selectChanged} ref={recencySelect}>
<option key={-1} value={-1}>All time</option> <option key={-1} value={-1}>{t('recency.all_time')}</option>
<option key={86400} value={86400}>Last day</option> <option key={86400} value={86400}>{t('recency.last_day')}</option>
<option key={604800} value={604800}>Last week </option> <option key={604800} value={604800}>{t('recency.last_week')}</option>
<option key={2629746} value={2629746}>Last month</option> <option key={2629746} value={2629746}>{t('recency.last_month')}</option>
<option key={7889238} value={7889238}>Last 3 months</option> <option key={7889238} value={7889238}>{t('recency.last_3_months')}</option>
<option key={15778476} value={15778476}>Last 6 months</option> <option key={15778476} value={15778476}>{t('recency.last_6_months')}</option>
<option key={31556952} value={31556952}>Last year</option> <option key={31556952} value={31556952}>{t('recency.last_year')}</option>
</select> </select>
</div> </div>
) )

View file

@ -1,6 +1,8 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next'
import classNames from 'classnames' import classNames from 'classnames'
import { accountState } from '~utils/accountState' import { accountState } from '~utils/accountState'
@ -30,6 +32,10 @@ const GridRep = (props: Props) => {
const { account } = useSnapshot(accountState) const { account } = useSnapshot(accountState)
const router = useRouter()
const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const [mainhand, setMainhand] = useState<Weapon>() const [mainhand, setMainhand] = useState<Weapon>()
const [weapons, setWeapons] = useState<GridArray<Weapon>>({}) const [weapons, setWeapons] = useState<GridArray<Weapon>>({})
@ -66,12 +72,12 @@ const GridRep = (props: Props) => {
function generateMainhandImage() { function generateMainhandImage() {
return (mainhand) ? return (mainhand) ?
<img alt={mainhand?.name.en} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand?.granblue_id}.jpg`} /> : '' <img alt={mainhand?.name[locale]} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand?.granblue_id}.jpg`} /> : ''
} }
function generateGridImage(position: number) { function generateGridImage(position: number) {
return (weapons[position]) ? return (weapons[position]) ?
<img alt={weapons[position]?.name.en} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapons[position]?.granblue_id}.jpg`} /> : '' <img alt={weapons[position]?.name[locale]} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapons[position]?.granblue_id}.jpg`} /> : ''
} }
function sendSaveData() { function sendSaveData() {
@ -96,22 +102,22 @@ const GridRep = (props: Props) => {
const details = ( const details = (
<div className="Details"> <div className="Details">
<h2 className={titleClass} onClick={navigate}>{ (props.name) ? props.name : 'Untitled' }</h2> <h2 className={titleClass} onClick={navigate}>{ (props.name) ? props.name : t('no_title') }</h2>
<div className="bottom"> <div className="bottom">
<div className={raidClass}>{ (props.raid) ? props.raid.name.en : 'No raid set' }</div> <div className={raidClass}>{ (props.raid) ? props.raid.name[locale] : t('no_raid') }</div>
<time className="last-updated" dateTime={props.createdAt.toISOString()}>{formatTimeAgo(props.createdAt, 'en-us')}</time> <time className="last-updated" dateTime={props.createdAt.toISOString()}>{formatTimeAgo(props.createdAt, locale)}</time>
</div> </div>
</div> </div>
) )
const detailsWithUsername = ( const detailsWithUsername = (
<div className="Details"> <div className="Details">
<div className="top"> <div className="top">
<div className="info"> <div className="info">
<h2 className={titleClass} onClick={navigate}>{ (props.name) ? props.name : 'Untitled' }</h2> <h2 className={titleClass} onClick={navigate}>{ (props.name) ? props.name : t('no_title') }</h2>
<div className={raidClass}>{ (props.raid) ? props.raid.name.en : 'No raid set' }</div> <div className={raidClass}>{ (props.raid) ? props.raid.name[locale] : t('no_raid') }</div>
</div> </div>
{ (!props.user || (account.user && account.user.id !== props.user.id)) ? { (account.authorized && (props.user && account.user && account.user.id !== props.user.id)) ?
<Button <Button
active={props.favorited} active={props.favorited}
icon="save" icon="save"
@ -122,9 +128,9 @@ const GridRep = (props: Props) => {
<div className="bottom"> <div className="bottom">
<div className={userClass}> <div className={userClass}>
{ userImage() } { userImage() }
{ (props.user) ? props.user.username : 'Anonymous' } { (props.user) ? props.user.username : t('no_user') }
</div> </div>
<time className="last-updated" dateTime={props.createdAt.toISOString()}>{formatTimeAgo(props.createdAt, 'en-us')}</time> <time className="last-updated" dateTime={props.createdAt.toISOString()}>{formatTimeAgo(props.createdAt, locale)}</time>
</div> </div>
</div> </div>
) )

View file

@ -26,6 +26,73 @@
padding: 6px 12px; padding: 6px 12px;
} }
&.language {
align-items: center;
display: flex;
flex-direction: row;
gap: $unit;
padding-right: $unit;
span {
flex-grow: 1;
}
.Switch {
$height: 24px;
background: $grey-60;
border-radius: calc($height / 2);
border: none;
position: relative;
width: 44px;
height: $height;
&:hover {
cursor: pointer;
}
.Thumb {
$diameter: 18px;
background: white;
border-radius: calc($diameter / 2);
display: block;
height: $diameter;
width: $diameter;
transition: transform 100ms;
transform: translateX(-2px);
z-index: 3;
&:hover {
cursor: pointer;
}
&[data-state="checked"] {
background: white;
transform: translateX(17px);
}
}
.left, .right {
color: white;
font-size: 10px;
font-weight: $bold;
position: absolute;
z-index: 2;
}
.left {
top: 6px;
left: 6px;
}
.right {
top: 6px;
right: 5px;
}
}
}
a { a {
color: $grey-40; color: $grey-40;
} }
@ -54,7 +121,7 @@
img { img {
$diameter: 32px; $diameter: 32px;
border-radius: $diameter / 2; border-radius: calc($diameter / 2);
height: $diameter; height: $diameter;
width: $diameter; width: $diameter;
} }

View file

@ -1,6 +1,10 @@
import React from 'react' import React, { useEffect, useState } from 'react'
import Link from 'next/link'
import { useCookies } from 'react-cookie' import { useCookies } from 'react-cookie'
import Router, { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import Link from 'next/link'
import * as Switch from '@radix-ui/react-switch'
import AboutModal from '~components/AboutModal' import AboutModal from '~components/AboutModal'
import AccountModal from '~components/AccountModal' import AccountModal from '~components/AccountModal'
@ -16,8 +20,27 @@ interface Props {
} }
const HeaderMenu = (props: Props) => { const HeaderMenu = (props: Props) => {
const router = useRouter()
const { t } = useTranslation('common')
const [accountCookies] = useCookies(['account']) const [accountCookies] = useCookies(['account'])
const [userCookies] = useCookies(['user']) const [userCookies] = useCookies(['user'])
const [cookies, setCookies] = useCookies()
const [checked, setChecked] = useState(false)
// console.log(`Currently: ${checked} ${cookies['NEXT_LOCALE']}`)
useEffect(() => {
const locale = cookies['NEXT_LOCALE']
setChecked((locale === 'ja') ? true : false)
}, [cookies])
function handleCheckedChange(value: boolean) {
const language = (value) ? 'ja' : 'en'
setCookies('NEXT_LOCALE', language, { path: '/'})
router.push(router.asPath, undefined, { locale: language })
}
function authItems() { function authItems() {
return ( return (
@ -35,22 +58,22 @@ const HeaderMenu = (props: Props) => {
/profile/${userCookies.user.picture}@2x.png 2x`} /profile/${userCookies.user.picture}@2x.png 2x`}
src={`/profile/${userCookies.user.picture}.png`} src={`/profile/${userCookies.user.picture}.png`}
/> />
</div </div>
></Link> </Link>
</li> </li>
<li className="MenuItem"> <li className="MenuItem">
<Link href={`/saved` || ''}>Saved</Link> <Link href={`/saved` || ''}>{t('menu.saved')}</Link>
</li> </li>
</div> </div>
<div className="MenuGroup"> <div className="MenuGroup">
<li className="MenuItem"> <li className="MenuItem">
<Link href='/teams'>Teams</Link> <Link href='/teams'>{t('menu.teams')}</Link>
</li> </li>
<li className="MenuItem disabled"> <li className="MenuItem disabled">
<div> <div>
<span>Guides</span> <span>{t('menu.guides')}</span>
<i className="tag">Coming Soon</i> <i className="tag">{t('coming_soon')}</i>
</div> </div>
</li> </li>
</div> </div>
@ -58,7 +81,7 @@ const HeaderMenu = (props: Props) => {
<AboutModal /> <AboutModal />
<AccountModal /> <AccountModal />
<li className="MenuItem" onClick={props.logout}> <li className="MenuItem" onClick={props.logout}>
<span>Logout</span> <span>{t('menu.logout')}</span>
</li> </li>
</div> </div>
</ul> </ul>
@ -70,20 +93,30 @@ const HeaderMenu = (props: Props) => {
return ( return (
<ul className="Menu unauth"> <ul className="Menu unauth">
<div className="MenuGroup"> <div className="MenuGroup">
<AboutModal /> <li className="MenuItem language">
<span>{t('menu.language')}</span>
<Switch.Root className="Switch" onCheckedChange={handleCheckedChange} checked={checked}>
<Switch.Thumb className="Thumb" />
<span className="left">JP</span>
<span className="right">EN</span>
</Switch.Root>
</li>
</div> </div>
<div className="MenuGroup"> <div className="MenuGroup">
<li className="MenuItem"> <li className="MenuItem">
<Link href='/teams'>Teams</Link> <Link href='/teams'>{t('menu.teams')}</Link>
</li> </li>
<li className="MenuItem disabled"> <li className="MenuItem disabled">
<div> <div>
<span>Guides</span> <span>{t('menu.guides')}</span>
<i className="tag">Coming Soon</i> <i className="tag">{t('coming_soon')}</i>
</div> </div>
</li> </li>
</div> </div>
<div className="MenuGroup">
<AboutModal />
</div>
<div className="MenuGroup"> <div className="MenuGroup">
<LoginModal /> <LoginModal />
<SignupModal /> <SignupModal />

View file

@ -1,5 +1,7 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { useCookies } from 'react-cookie' import { useCookies } from 'react-cookie'
import Router, { useRouter } from 'next/router'
import { useTranslation } from 'react-i18next'
import { AxiosResponse } from 'axios' import { AxiosResponse } from 'axios'
import * as Dialog from '@radix-ui/react-dialog' import * as Dialog from '@radix-ui/react-dialog'
@ -24,6 +26,9 @@ interface ErrorMap {
const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const LoginModal = (props: Props) => { const LoginModal = (props: Props) => {
const router = useRouter()
const { t } = useTranslation('common')
// Set up form states and error handling // Set up form states and error handling
const [formValid, setFormValid] = useState(false) const [formValid, setFormValid] = useState(false)
const [errors, setErrors] = useState<ErrorMap>({ const [errors, setErrors] = useState<ErrorMap>({
@ -49,16 +54,16 @@ const LoginModal = (props: Props) => {
switch(name) { switch(name) {
case 'email': case 'email':
if (value.length == 0) if (value.length == 0)
newErrors.email = 'Please enter your email' newErrors.email = t('modals.login.errors.empty_email')
else if (!emailRegex.test(value)) else if (!emailRegex.test(value))
newErrors.email = 'That email address is not valid' newErrors.email = t('modals.login.errors.invalid_email')
else else
newErrors.email = '' newErrors.email = ''
break break
case 'password': case 'password':
newErrors.password = value.length == 0 newErrors.password = value.length == 0
? 'Please enter your password' ? t('modals.login.errors.empty_password')
: '' : ''
break break
@ -117,7 +122,7 @@ const LoginModal = (props: Props) => {
access_token: response.data.access_token access_token: response.data.access_token
} }
setCookies('account', cookieObj, { path: '/'}) setCookies('account', cookieObj, { path: '/' })
} }
function storeUserInfo(response: AxiosResponse) { function storeUserInfo(response: AxiosResponse) {
@ -129,9 +134,8 @@ const LoginModal = (props: Props) => {
language: user.language, language: user.language,
} }
setCookies('user', cookieObj, { path: '/'}) setCookies('user', cookieObj, { path: '/' })
accountState.account.language = user.language
accountState.account.user = { accountState.account.user = {
id: user.id, id: user.id,
username: user.username, username: user.username,
@ -140,7 +144,16 @@ const LoginModal = (props: Props) => {
} }
accountState.account.authorized = true accountState.account.authorized = true
setOpen(false) setOpen(false)
changeLanguage(user.language)
}
function changeLanguage(newLanguage: string) {
if (newLanguage !== router.locale) {
setCookies('NEXT_LOCALE', newLanguage, { path: '/'})
router.push(router.asPath, undefined, { locale: newLanguage })
}
} }
function openChange(open: boolean) { function openChange(open: boolean) {
@ -155,13 +168,13 @@ const LoginModal = (props: Props) => {
<Dialog.Root open={open} onOpenChange={openChange}> <Dialog.Root open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>
<li className="MenuItem"> <li className="MenuItem">
<span>Log in</span> <span>{t('menu.login')}</span>
</li> </li>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Content className="Login Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }> <Dialog.Content className="Login Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }>
<div className="DialogHeader"> <div className="DialogHeader">
<Dialog.Title className="DialogTitle">Log in</Dialog.Title> <Dialog.Title className="DialogTitle">{t('modals.login.title')}</Dialog.Title>
<Dialog.Close className="DialogClose" asChild> <Dialog.Close className="DialogClose" asChild>
<span> <span>
<CrossIcon /> <CrossIcon />
@ -172,7 +185,7 @@ const LoginModal = (props: Props) => {
<form className="form" onSubmit={login}> <form className="form" onSubmit={login}>
<Fieldset <Fieldset
fieldName="email" fieldName="email"
placeholder="Email address" placeholder={t('modals.login.placeholders.email')}
onChange={handleChange} onChange={handleChange}
error={errors.email} error={errors.email}
ref={emailInput} ref={emailInput}
@ -180,13 +193,13 @@ const LoginModal = (props: Props) => {
<Fieldset <Fieldset
fieldName="password" fieldName="password"
placeholder="Password" placeholder={t('modals.login.placeholders.password')}
onChange={handleChange} onChange={handleChange}
error={errors.password} error={errors.password}
ref={passwordInput} ref={passwordInput}
/> />
<Button>Log in</Button> <Button>{t('modals.login.buttons.confirm')}</Button>
</form> </form>
</Dialog.Content> </Dialog.Content>
<Dialog.Overlay className="Overlay" /> <Dialog.Overlay className="Overlay" />

View file

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import './index.scss' import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next'
import { appState } from '~utils/appState' import { appState } from '~utils/appState'
@ -8,7 +9,9 @@ import Segment from '~components/Segment'
import ToggleSwitch from '~components/ToggleSwitch' import ToggleSwitch from '~components/ToggleSwitch'
import { GridType } from '~utils/enums' import { GridType } from '~utils/enums'
import { useSnapshot } from 'valtio'
import './index.scss'
interface Props { interface Props {
selectedTab: GridType selectedTab: GridType
@ -17,6 +20,8 @@ interface Props {
} }
const PartySegmentedControl = (props: Props) => { const PartySegmentedControl = (props: Props) => {
const { t } = useTranslation('common')
const { party, grid } = useSnapshot(appState) const { party, grid } = useSnapshot(appState)
function getElement() { function getElement() {
@ -62,21 +67,21 @@ const PartySegmentedControl = (props: Props) => {
name="characters" name="characters"
selected={props.selectedTab == GridType.Character} selected={props.selectedTab == GridType.Character}
onClick={props.onClick} onClick={props.onClick}
>Characters</Segment> >{t('party.segmented_control.characters')}</Segment>
<Segment <Segment
groupName="grid" groupName="grid"
name="weapons" name="weapons"
selected={props.selectedTab == GridType.Weapon} selected={props.selectedTab == GridType.Weapon}
onClick={props.onClick} onClick={props.onClick}
>Weapons</Segment> >{t('party.segmented_control.weapons')}</Segment>
<Segment <Segment
groupName="grid" groupName="grid"
name="summons" name="summons"
selected={props.selectedTab == GridType.Summon} selected={props.selectedTab == GridType.Summon}
onClick={props.onClick} onClick={props.onClick}
>Summons</Segment> >{t('party.segmented_control.summons')}</Segment>
</SegmentedControl> </SegmentedControl>
{ {

View file

@ -1,4 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import { appState } from '~utils/appState' import { appState } from '~utils/appState'
import api from '~utils/api' import api from '~utils/api'
@ -14,6 +16,10 @@ interface Props {
} }
const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFieldSet(props, ref) { const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFieldSet(props, ref) {
const router = useRouter()
const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const [raids, setRaids] = useState<Raid[][]>() const [raids, setRaids] = useState<Raid[][]>()
const raidGroups = [ const raidGroups = [
@ -37,7 +43,7 @@ const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFiel
id: '0', id: '0',
name: { name: {
en: 'All raids', en: 'All raids',
jp: '全て' ja: '全てのマルチ'
}, },
level: 0, level: 0,
group: 0, group: 0,
@ -65,7 +71,7 @@ const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFiel
const options = raids && raids.length > 0 && raids[index].length > 0 && const options = raids && raids.length > 0 && raids[index].length > 0 &&
raids[index].sort((a, b) => a.element - b.element).map((item, i) => { raids[index].sort((a, b) => a.element - b.element).map((item, i) => {
return ( return (
<option key={i} value={item.id}>{item.name.en}</option> <option key={i} value={item.id}>{item.name[locale]}</option>
) )
}) })

View file

@ -1,5 +1,8 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import Link from 'next/link'
import { useCookies } from 'react-cookie' import { useCookies } from 'react-cookie'
import { useRouter } from 'next/router'
import { Trans, useTranslation } from 'next-i18next'
import { AxiosResponse } from 'axios' import { AxiosResponse } from 'axios'
import * as Dialog from '@radix-ui/react-dialog' import * as Dialog from '@radix-ui/react-dialog'
@ -26,6 +29,9 @@ interface ErrorMap {
const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const SignupModal = (props: Props) => { const SignupModal = (props: Props) => {
const router = useRouter()
const { t } = useTranslation('common')
// Set up form states and error handling // Set up form states and error handling
const [formValid, setFormValid] = useState(false) const [formValid, setFormValid] = useState(false)
const [errors, setErrors] = useState<ErrorMap>({ const [errors, setErrors] = useState<ErrorMap>({
@ -56,7 +62,8 @@ const SignupModal = (props: Props) => {
username: usernameInput.current?.value, username: usernameInput.current?.value,
email: emailInput.current?.value, email: emailInput.current?.value,
password: passwordInput.current?.value, password: passwordInput.current?.value,
password_confirmation: passwordConfirmationInput.current?.value password_confirmation: passwordConfirmationInput.current?.value,
language: router.locale
} }
} }
@ -95,9 +102,9 @@ const SignupModal = (props: Props) => {
language: user.language, language: user.language,
} }
// TODO: Set language
setCookies('user', cookieObj, { path: '/'}) setCookies('user', cookieObj, { path: '/'})
accountState.account.language = user.language
accountState.account.user = { accountState.account.user = {
id: user.id, id: user.id,
username: user.username, username: user.username,
@ -138,7 +145,7 @@ const SignupModal = (props: Props) => {
validateName(fieldName, value) validateName(fieldName, value)
} else { } else {
newErrors[fieldName] = `This ${fieldName} is already in use` newErrors[fieldName] = t('modals.signup.errors.field_in_use', { field: fieldName})
setErrors(newErrors) setErrors(newErrors)
setFormValid(false) setFormValid(false)
} }
@ -150,9 +157,9 @@ const SignupModal = (props: Props) => {
switch(fieldName) { switch(fieldName) {
case 'username': case 'username':
if (value.length < 3) if (value.length < 3)
newErrors.username = 'Username must be at least 3 characters' newErrors.username = t('modals.signup.errors.username_too_short')
else if (value.length > 20) else if (value.length > 20)
newErrors.username = 'Username must be less than 20 characters' newErrors.username = t('modals.signup.errors.username_too_long')
else else
newErrors.username = '' newErrors.username = ''
@ -161,7 +168,7 @@ const SignupModal = (props: Props) => {
case 'email': case 'email':
newErrors.email = emailRegex.test(value) newErrors.email = emailRegex.test(value)
? '' ? ''
: 'That email address is not valid' : t('modals.signup.errors.invalid_email')
break break
default: default:
@ -180,20 +187,20 @@ const SignupModal = (props: Props) => {
switch(name) { switch(name) {
case 'password': case 'password':
newErrors.password = passwordInput.current?.value.includes(usernameInput.current?.value!) newErrors.password = passwordInput.current?.value.includes(usernameInput.current?.value!)
? 'Your password should not contain your username' ? t('modals.signup.errors.password_contains_username')
: '' : ''
break break
case 'password': case 'password':
newErrors.password = value.length < 8 newErrors.password = value.length < 8
? 'Password must be at least 8 characters' ? t('modals.signup.errors.password_too_short')
: '' : ''
break break
case 'confirm_password': case 'confirm_password':
newErrors.passwordConfirmation = passwordInput.current?.value === passwordConfirmationInput.current?.value newErrors.passwordConfirmation = passwordInput.current?.value === passwordConfirmationInput.current?.value
? '' ? ''
: 'Your passwords don\'t match' : t('modals.signup.errors.passwords_dont_match')
break break
default: default:
@ -231,13 +238,13 @@ const SignupModal = (props: Props) => {
<Dialog.Root open={open} onOpenChange={openChange}> <Dialog.Root open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>
<li className="MenuItem"> <li className="MenuItem">
<span>Sign up</span> <span>{t('menu.signup')}</span>
</li> </li>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Content className="Signup Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }> <Dialog.Content className="Signup Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }>
<div className="DialogHeader"> <div className="DialogHeader">
<Dialog.Title className="DialogTitle">Sign up</Dialog.Title> <Dialog.Title className="DialogTitle">{t('modals.signup.title')}</Dialog.Title>
<Dialog.Close className="DialogClose" asChild> <Dialog.Close className="DialogClose" asChild>
<span> <span>
<CrossIcon /> <CrossIcon />
@ -248,7 +255,7 @@ const SignupModal = (props: Props) => {
<form className="form" onSubmit={register}> <form className="form" onSubmit={register}>
<Fieldset <Fieldset
fieldName="username" fieldName="username"
placeholder="Username" placeholder={t('modals.signup.placeholders.username')}
onChange={handleNameChange} onChange={handleNameChange}
error={errors.username} error={errors.username}
ref={usernameInput} ref={usernameInput}
@ -256,7 +263,7 @@ const SignupModal = (props: Props) => {
<Fieldset <Fieldset
fieldName="email" fieldName="email"
placeholder="Email address" placeholder={t('modals.signup.placeholders.email')}
onChange={handleNameChange} onChange={handleNameChange}
error={errors.email} error={errors.email}
ref={emailInput} ref={emailInput}
@ -264,7 +271,7 @@ const SignupModal = (props: Props) => {
<Fieldset <Fieldset
fieldName="password" fieldName="password"
placeholder="Password" placeholder={t('modals.signup.placeholders.password')}
onChange={handlePasswordChange} onChange={handlePasswordChange}
error={errors.password} error={errors.password}
ref={passwordInput} ref={passwordInput}
@ -272,16 +279,18 @@ const SignupModal = (props: Props) => {
<Fieldset <Fieldset
fieldName="confirm_password" fieldName="confirm_password"
placeholder="Password (again)" placeholder={t('modals.signup.placeholders.password_confirm')}
onChange={handlePasswordChange} onChange={handlePasswordChange}
error={errors.passwordConfirmation} error={errors.passwordConfirmation}
ref={passwordConfirmationInput} ref={passwordConfirmationInput}
/> />
<Button>Sign up</Button> <Button>{t('modals.signup.buttons.confirm')}</Button>
<Dialog.Description className="terms"> <Dialog.Description className="terms">
By signing up, I agree to the<br /><a href="#">Terms and Conditions</a> and <a href="#">Usage Guidelines</a>. <Trans i18nKey="modals.signup.agreement">
By signing up, I agree to the <Link href="/privacy">Privacy Policy</Link><Link href="/usage">Usage Guidelines</Link>.
</Trans>
</Dialog.Description> </Dialog.Description>
</form> </form>
</Dialog.Content> </Dialog.Content>

View file

@ -2,6 +2,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useCookies } from 'react-cookie' import { useCookies } from 'react-cookie'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next'
import { AxiosResponse } from 'axios' import { AxiosResponse } from 'axios'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
@ -26,6 +27,8 @@ const SummonGrid = (props: Props) => {
// Constants // Constants
const numSummons: number = 4 const numSummons: number = 4
const { t } = useTranslation('common')
// Cookies // Cookies
const [cookies, _] = useCookies(['account']) const [cookies, _] = useCookies(['account'])
const headers = (cookies.account != null) ? { const headers = (cookies.account != null) ? {
@ -239,7 +242,7 @@ const SummonGrid = (props: Props) => {
// Render: JSX components // Render: JSX components
const mainSummonElement = ( const mainSummonElement = (
<div className="LabeledUnit"> <div className="LabeledUnit">
<div className="Label">Main Summon</div> <div className="Label">{t('summons.main')}</div>
<SummonUnit <SummonUnit
gridSummon={grid.summons.mainSummon} gridSummon={grid.summons.mainSummon}
editable={party.editable} editable={party.editable}
@ -254,7 +257,7 @@ const SummonGrid = (props: Props) => {
const friendSummonElement = ( const friendSummonElement = (
<div className="LabeledUnit"> <div className="LabeledUnit">
<div className="Label">Friend Summon</div> <div className="Label">{t('summons.friend')}</div>
<SummonUnit <SummonUnit
gridSummon={grid.summons.friendSummon} gridSummon={grid.summons.friendSummon}
editable={party.editable} editable={party.editable}
@ -268,7 +271,7 @@ const SummonGrid = (props: Props) => {
) )
const summonGridElement = ( const summonGridElement = (
<div id="LabeledGrid"> <div id="LabeledGrid">
<div className="Label">Summons</div> <div className="Label">{t('summons.summons')}</div>
<ul id="grid_summons"> <ul id="grid_summons">
{Array.from(Array(numSummons)).map((x, i) => { {Array.from(Array(numSummons)).map((x, i) => {
return (<li key={`grid_unit_${i}`} > return (<li key={`grid_unit_${i}`} >

View file

@ -1,11 +1,12 @@
import React from 'react' import React from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import * as HoverCard from '@radix-ui/react-hover-card' 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 './index.scss' import './index.scss'
interface Props { interface Props {
@ -21,8 +22,11 @@ interface KeyNames {
} }
const SummonHovercard = (props: Props) => { const SummonHovercard = (props: Props) => {
const router = useRouter()
const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light'] const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana']
const tintElement = Element[props.gridSummon.object.element] const tintElement = Element[props.gridSummon.object.element]
const wikiUrl = `https://gbf.wiki/${props.gridSummon.object.name.en.replaceAll(' ', '_')}` const wikiUrl = `https://gbf.wiki/${props.gridSummon.object.name.en.replaceAll(' ', '_')}`
@ -57,8 +61,8 @@ const SummonHovercard = (props: Props) => {
<HoverCard.Content className="Weapon Hovercard"> <HoverCard.Content className="Weapon Hovercard">
<div className="top"> <div className="top">
<div className="title"> <div className="title">
<h4>{ props.gridSummon.object.name.en }</h4> <h4>{ props.gridSummon.object.name[locale] }</h4>
<img alt={props.gridSummon.object.name.en} src={summonImage()} /> <img alt={props.gridSummon.object.name[locale]} src={summonImage()} />
</div> </div>
<div className="subInfo"> <div className="subInfo">
<div className="icons"> <div className="icons">
@ -72,7 +76,7 @@ const SummonHovercard = (props: Props) => {
/> />
</div> </div>
</div> </div>
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">View more on gbf.wiki</a> <a className={`Button ${tintElement}`} href={wikiUrl} target="_new">{t('buttons.wiki')}</a>
<HoverCard.Arrow /> <HoverCard.Arrow />
</HoverCard.Content> </HoverCard.Content>
</HoverCard.Root> </HoverCard.Root>

View file

@ -1,4 +1,6 @@
import React from 'react' import React from 'react'
import { useRouter } from 'next/router'
import UncapIndicator from '~components/UncapIndicator' import UncapIndicator from '~components/UncapIndicator'
import WeaponLabelIcon from '~components/WeaponLabelIcon' import WeaponLabelIcon from '~components/WeaponLabelIcon'
@ -11,28 +13,29 @@ interface Props {
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light'] const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
class SummonResult extends React.Component<Props> { const SummonResult = (props: Props) => {
render() { const router = useRouter()
const summon = this.props.data const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
return ( const summon = props.data
<li className="SummonResult" onClick={this.props.onClick}>
<img alt={summon.name.en} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}.jpg`} /> return (
<div className="Info"> <li className="SummonResult" onClick={props.onClick}>
<h5>{summon.name.en}</h5> <img alt={summon.name[locale]} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}.jpg`} />
<UncapIndicator <div className="Info">
type="summon" <h5>{summon.name[locale]}</h5>
flb={summon.uncap.flb} <UncapIndicator
ulb={summon.uncap.ulb} type="summon"
special={false} flb={summon.uncap.flb}
/> ulb={summon.uncap.ulb}
<div className="tags"> special={false}
<WeaponLabelIcon labelType={Element[summon.element]} /> />
</div> <div className="tags">
<WeaponLabelIcon labelType={Element[summon.element]} />
</div> </div>
</li> </div>
) </li>
} )
} }
export default SummonResult export default SummonResult

View file

@ -1,4 +1,6 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import classnames from 'classnames' import classnames from 'classnames'
import SearchModal from '~components/SearchModal' import SearchModal from '~components/SearchModal'
@ -18,8 +20,13 @@ interface Props {
} }
const SummonUnit = (props: Props) => { const SummonUnit = (props: Props) => {
const { t } = useTranslation('common')
const [imageUrl, setImageUrl] = useState('') const [imageUrl, setImageUrl] = useState('')
const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const classes = classnames({ const classes = classnames({
SummonUnit: true, SummonUnit: true,
'main': props.unitType == 0, 'main': props.unitType == 0,
@ -76,7 +83,7 @@ const SummonUnit = (props: Props) => {
const editableImage = ( const editableImage = (
<SearchModal <SearchModal
placeholderText="Search for a summon..." placeholderText={t('search.placeholders.summon')}
fromPosition={props.position} fromPosition={props.position}
object="summons" object="summons"
send={props.updateObject}> send={props.updateObject}>
@ -97,7 +104,7 @@ const SummonUnit = (props: Props) => {
special={false} special={false}
/> : '' /> : ''
} }
<h3 className="SummonName">{summon?.name.en}</h3> <h3 className="SummonName">{summon?.name[locale]}</h3>
</div> </div>
) )

View file

@ -1,8 +1,10 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useSnapshot } from 'valtio'
import { useCookies } from 'react-cookie' import { useCookies } from 'react-cookie'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import clonedeep from 'lodash.clonedeep' import clonedeep from 'lodash.clonedeep'
import { useSnapshot } from 'valtio'
import api from '~utils/api' import api from '~utils/api'
import { accountState, initialAccountState } from '~utils/accountState' import { accountState, initialAccountState } from '~utils/accountState'
@ -13,6 +15,8 @@ import Button from '~components/Button'
import HeaderMenu from '~components/HeaderMenu' import HeaderMenu from '~components/HeaderMenu'
const TopHeader = () => { const TopHeader = () => {
const { t } = useTranslation('common')
// Cookies // Cookies
const [accountCookies, setAccountCookie, removeAccountCookie] = useCookies(['account']) const [accountCookies, setAccountCookie, removeAccountCookie] = useCookies(['account'])
const [userCookies, setUserCookies, removeUserCookie] = useCookies(['user']) const [userCookies, setUserCookies, removeUserCookie] = useCookies(['user'])
@ -100,7 +104,7 @@ const TopHeader = () => {
const leftNav = () => { const leftNav = () => {
return ( return (
<div className="dropdown"> <div className="dropdown">
<Button icon="menu">Menu</Button> <Button icon="menu">{t('buttons.menu')}</Button>
{ (account.user) ? { (account.user) ?
<HeaderMenu authenticated={account.authorized} username={account.user.username} logout={logout} /> : <HeaderMenu authenticated={account.authorized} username={account.user.username} logout={logout} /> :
<HeaderMenu authenticated={account.authorized} /> <HeaderMenu authenticated={account.authorized} />
@ -123,9 +127,9 @@ const TopHeader = () => {
saveButton() : '' saveButton() : ''
} }
{ (router.route === '/p/[party]') ? { (router.route === '/p/[party]') ?
<Button icon="link" onClick={copyToClipboard}>Copy link</Button> : '' <Button icon="link" onClick={copyToClipboard}>{t('buttons.copy')}</Button> : ''
} }
<Button icon="new" onClick={newParty}>New</Button> <Button icon="new" onClick={newParty}>{t('buttons.new')}</Button>
</div> </div>
) )
} }

View file

@ -1,4 +1,7 @@
import React from 'react' import React from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import * as HoverCard from '@radix-ui/react-hover-card' import * as HoverCard from '@radix-ui/react-hover-card'
import WeaponLabelIcon from '~components/WeaponLabelIcon' import WeaponLabelIcon from '~components/WeaponLabelIcon'
@ -15,30 +18,35 @@ interface Props {
interface KeyNames { interface KeyNames {
[key: string]: { [key: string]: {
[key: string]: string
en: string, en: string,
jp: string ja: string
} }
} }
const WeaponHovercard = (props: Props) => { const WeaponHovercard = (props: Props) => {
const router = useRouter()
const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light'] const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana'] const Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana']
const WeaponKeyNames: KeyNames = { const WeaponKeyNames: KeyNames = {
'2': { '2': {
en: 'Pendulum', en: 'Pendulum',
jp: '' ja: 'ペンデュラム'
}, },
'3': { '3': {
en: 'Teluma', en: 'Teluma',
jp: '' ja: 'テルマ'
}, },
'17': { '17': {
en: 'Gauph Key', en: 'Gauph Key',
jp: '' ja: 'ガフスキー'
}, },
'22': { '22': {
en: 'Emblem', en: 'Emblem',
jp: '' ja: 'エンブレム'
} }
} }
@ -61,7 +69,7 @@ const WeaponHovercard = (props: Props) => {
const simpleAxSkill = props.gridWeapon.ax[0] const simpleAxSkill = props.gridWeapon.ax[0]
const axSkill = primaryAxSkills.find(skill => skill.id == simpleAxSkill.modifier) const axSkill = primaryAxSkills.find(skill => skill.id == simpleAxSkill.modifier)
return `${axSkill?.name.en} +${simpleAxSkill.strength}${ (axSkill?.suffix) ? axSkill.suffix : '' }` return `${axSkill?.name[locale]} +${simpleAxSkill.strength}${ (axSkill?.suffix) ? axSkill.suffix : '' }`
} }
return '' return ''
@ -78,7 +86,7 @@ const WeaponHovercard = (props: Props) => {
if (primaryAxSkill && primaryAxSkill.secondary) { if (primaryAxSkill && primaryAxSkill.secondary) {
const secondaryAxSkill = primaryAxSkill.secondary.find(skill => skill.id == secondarySimpleAxSkill.modifier) const secondaryAxSkill = primaryAxSkill.secondary.find(skill => skill.id == secondarySimpleAxSkill.modifier)
return `${secondaryAxSkill?.name.en} +${secondarySimpleAxSkill.strength}${ (secondaryAxSkill?.suffix) ? secondaryAxSkill.suffix : '' }` return `${secondaryAxSkill?.name[locale]} +${secondarySimpleAxSkill.strength}${ (secondaryAxSkill?.suffix) ? secondaryAxSkill.suffix : '' }`
} }
} }
@ -97,14 +105,14 @@ const WeaponHovercard = (props: Props) => {
const keysSection = ( const keysSection = (
<section className="weaponKeys"> <section className="weaponKeys">
{ (WeaponKeyNames[props.gridWeapon.object.series]) ? { (WeaponKeyNames[props.gridWeapon.object.series]) ?
<h5 className={tintElement}>{ WeaponKeyNames[props.gridWeapon.object.series].en }s</h5> : '' <h5 className={tintElement}>{ WeaponKeyNames[props.gridWeapon.object.series][locale] }{ (locale === 'en') ? 's' : '' }</h5> : ''
} }
{ (props.gridWeapon.weapon_keys) ? { (props.gridWeapon.weapon_keys) ?
Array.from(Array(props.gridWeapon.weapon_keys.length)).map((x, i) => { Array.from(Array(props.gridWeapon.weapon_keys.length)).map((x, i) => {
return ( return (
<div className="weaponKey" key={props.gridWeapon.weapon_keys![i].id}> <div className="weaponKey" key={props.gridWeapon.weapon_keys![i].id}>
<span>{props.gridWeapon.weapon_keys![i].name.en}</span> <span>{props.gridWeapon.weapon_keys![i].name[locale]}</span>
</div> </div>
) )
}) : '' } }) : '' }
@ -113,16 +121,16 @@ const WeaponHovercard = (props: Props) => {
const axSection = ( const axSection = (
<section className="axSkills"> <section className="axSkills">
<h5 className={tintElement}>AX Skills</h5> <h5 className={tintElement}>{t('modals.weapon.subtitles.ax_skills')}</h5>
<div className="skills"> <div className="skills">
<div className="primary axSkill"> <div className="primary axSkill">
<img src={`/icons/ax/primary_${ (props.gridWeapon.ax) ? props.gridWeapon.ax[0].modifier : '' }.png`} /> <img alt="AX1" src={`/icons/ax/primary_${ (props.gridWeapon.ax) ? props.gridWeapon.ax[0].modifier : '' }.png`} />
<span>{createPrimaryAxSkillString()}</span> <span>{createPrimaryAxSkillString()}</span>
</div> </div>
{ (props.gridWeapon.ax && props.gridWeapon.ax[1].modifier && props.gridWeapon.ax[1].strength) ? { (props.gridWeapon.ax && props.gridWeapon.ax[1].modifier && props.gridWeapon.ax[1].strength) ?
<div className="secondary axSkill"> <div className="secondary axSkill">
<img src={`/icons/ax/secondary_${ (props.gridWeapon.ax) ? props.gridWeapon.ax[1].modifier : '' }.png`} /> <img alt="AX2" src={`/icons/ax/secondary_${ (props.gridWeapon.ax) ? props.gridWeapon.ax[1].modifier : '' }.png`} />
<span>{createSecondaryAxSkillString()}</span> <span>{createSecondaryAxSkillString()}</span>
</div> : ''} </div> : ''}
</div> </div>
@ -137,8 +145,8 @@ const WeaponHovercard = (props: Props) => {
<HoverCard.Content className="Weapon Hovercard" side={hovercardSide()}> <HoverCard.Content className="Weapon Hovercard" side={hovercardSide()}>
<div className="top"> <div className="top">
<div className="title"> <div className="title">
<h4>{ props.gridWeapon.object.name.en }</h4> <h4>{ props.gridWeapon.object.name[locale] }</h4>
<img alt={props.gridWeapon.object.name.en} src={weaponImage()} /> <img alt={props.gridWeapon.object.name[locale]} src={weaponImage()} />
</div> </div>
<div className="subInfo"> <div className="subInfo">
<div className="icons"> <div className="icons">
@ -158,7 +166,7 @@ const WeaponHovercard = (props: Props) => {
{ (props.gridWeapon.object.ax > 0 && props.gridWeapon.ax && props.gridWeapon.ax[0].modifier && props.gridWeapon.ax[0].strength ) ? axSection : '' } { (props.gridWeapon.object.ax > 0 && props.gridWeapon.ax && props.gridWeapon.ax[0].modifier && props.gridWeapon.ax[0].strength ) ? axSection : '' }
{ (props.gridWeapon.weapon_keys && props.gridWeapon.weapon_keys.length > 0) ? keysSection : '' } { (props.gridWeapon.weapon_keys && props.gridWeapon.weapon_keys.length > 0) ? keysSection : '' }
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">View more on gbf.wiki</a> <a className={`Button ${tintElement}`} href={wikiUrl} target="_new">{t('buttons.wiki')}</a>
<HoverCard.Arrow /> <HoverCard.Arrow />
</HoverCard.Content> </HoverCard.Content>
</HoverCard.Root> </HoverCard.Root>

View file

@ -6,74 +6,142 @@
/* Elements */ /* Elements */
&.fire { &.fire.en {
background-image: url('/labels/element/Label_Element_Fire.png') background-image: url('/labels/element/fire_en.png')
} }
&.water { &.fire.ja {
background-image: url('/labels/element/Label_Element_Water.png') background-image: url('/labels/element/fire_ja.png')
} }
&.earth { &.water.en {
background-image: url('/labels/element/Label_Element_Earth.png') background-image: url('/labels/element/water_en.png')
} }
&.wind { &.water.ja {
background-image: url('/labels/element/Label_Element_Wind.png') background-image: url('/labels/element/water_ja.png')
} }
&.dark { &.earth.en {
background-image: url('/labels/element/Label_Element_Dark.png') background-image: url('/labels/element/earth_en.png')
} }
&.light { &.earth.ja {
background-image: url('/labels/element/Label_Element_Light.png') background-image: url('/labels/element/earth_ja.png')
} }
&.null { &.wind.en {
background-image: url('/labels/element/Label_Element_Any.png') background-image: url('/labels/element/wind_en.png')
}
&.wind.ja {
background-image: url('/labels/element/wind_ja.png')
}
&.dark.en {
background-image: url('/labels/element/dark_en.png')
}
&.dark.ja {
background-image: url('/labels/element/dark_ja.png')
}
&.light.en {
background-image: url('/labels/element/light_en.png')
}
&.light.ja {
background-image: url('/labels/element/light_ja.png')
}
&.null.en {
background-image: url('/labels/element/any_en.png')
}
&.null.ja {
background-image: url('/labels/element/any_ja.png')
} }
/* Proficiencies */ /* Proficiencies */
&.sword { &.sword.en {
background-image: url('/labels/proficiency/Label_Weapon_Sabre.png') background-image: url('/labels/proficiency/sabre_en.png')
} }
&.dagger { &.sword.ja {
background-image: url('/labels/proficiency/Label_Weapon_Dagger.png') background-image: url('/labels/proficiency/sabre_ja.png')
} }
&.axe { &.dagger.en {
background-image: url('/labels/proficiency/Label_Weapon_Axe.png') background-image: url('/labels/proficiency/dagger_en.png')
} }
&.spear { &.dagger.ja {
background-image: url('/labels/proficiency/Label_Weapon_Spear.png') background-image: url('/labels/proficiency/dagger_ja.png')
} }
&.staff { &.axe.en {
background-image: url('/labels/proficiency/Label_Weapon_Staff.png') background-image: url('/labels/proficiency/axe_en.png')
} }
&.fist { &.axe.ja {
background-image: url('/labels/proficiency/Label_Weapon_Melee.png') background-image: url('/labels/proficiency/axe_ja.png')
} }
&.harp { &.spear.en {
background-image: url('/labels/proficiency/Label_Weapon_Harp.png') background-image: url('/labels/proficiency/spear_en.png')
} }
&.gun { &.spear.ja {
background-image: url('/labels/proficiency/Label_Weapon_Gun.png') background-image: url('/labels/proficiency/spear_ja.png')
} }
&.bow { &.staff.en {
background-image: url('/labels/proficiency/Label_Weapon_Bow.png') background-image: url('/labels/proficiency/staff_en.png')
} }
&.katana { &.staff.ja {
background-image: url('/labels/proficiency/Label_Weapon_Katana.png') background-image: url('/labels/proficiency/staff_ja.png')
}
&.fist.en {
background-image: url('/labels/proficiency/melee_en.png')
}
&.fist.ja {
background-image: url('/labels/proficiency/melee_ja.png')
}
&.harp.en {
background-image: url('/labels/proficiency/harp_en.png')
}
&.harp.ja {
background-image: url('/labels/proficiency/harp_ja.png')
}
&.gun.en {
background-image: url('/labels/proficiency/gun_en.png')
}
&.gun.ja {
background-image: url('/labels/proficiency/gun_ja.png')
}
&.bow.en {
background-image: url('/labels/proficiency/bow_en.png')
}
&.bow.ja {
background-image: url('/labels/proficiency/bow_ja.png')
}
&.katana.en {
background-image: url('/labels/proficiency/katana_en.png')
}
&.katana.ja {
background-image: url('/labels/proficiency/katana_ja.png')
} }
} }

View file

@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import { useRouter } from 'next/router'
import './index.scss' import './index.scss'
@ -6,12 +7,12 @@ interface Props {
labelType: string labelType: string
} }
class WeaponLabelIcon extends React.Component<Props> { const WeaponLabelIcon = (props: Props) => {
render() { const router = useRouter()
return (
<i className={`WeaponLabelIcon ${this.props.labelType}`} /> return (
) <i className={`WeaponLabelIcon ${props.labelType} ${router.locale}`} />
} )
} }
export default WeaponLabelIcon export default WeaponLabelIcon

View file

@ -1,6 +1,9 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { useCookies } from 'react-cookie' import { useCookies } from 'react-cookie'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import { AxiosResponse } from 'axios' import { AxiosResponse } from 'axios'
import * as Dialog from '@radix-ui/react-dialog' import * as Dialog from '@radix-ui/react-dialog'
import AXSelect from '~components/AxSelect' import AXSelect from '~components/AxSelect'
@ -33,6 +36,10 @@ interface Props {
} }
const WeaponModal = (props: Props) => { const WeaponModal = (props: Props) => {
const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const { t } = useTranslation('common')
// Cookies // Cookies
const [cookies] = useCookies(['account']) const [cookies] = useCookies(['account'])
const headers = (cookies.account != null) ? { const headers = (cookies.account != null) ? {
@ -122,7 +129,7 @@ const WeaponModal = (props: Props) => {
const elementSelect = () => { const elementSelect = () => {
return ( return (
<section> <section>
<h3>Element</h3> <h3>{t('modals.weapon.subtitles.element')}</h3>
<ElementToggle <ElementToggle
currentElement={props.gridWeapon.element} currentElement={props.gridWeapon.element}
sendValue={receiveElementValue} sendValue={receiveElementValue}
@ -134,7 +141,7 @@ const WeaponModal = (props: Props) => {
const keySelect = () => { const keySelect = () => {
return ( return (
<section> <section>
<h3>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(props.gridWeapon.object.series)) ?
<WeaponKeyDropdown <WeaponKeyDropdown
currentValue={ (props.gridWeapon.weapon_keys) ? props.gridWeapon.weapon_keys[0] : undefined } currentValue={ (props.gridWeapon.weapon_keys) ? props.gridWeapon.weapon_keys[0] : undefined }
@ -165,6 +172,7 @@ const WeaponModal = (props: Props) => {
const axSelect = () => { const axSelect = () => {
return ( return (
<section> <section>
<h3>{t('modals.weapon.subtitles.ax_skills')}</h3>
<AXSelect <AXSelect
axType={props.gridWeapon.object.ax} axType={props.gridWeapon.object.ax}
currentSkills={props.gridWeapon.ax} currentSkills={props.gridWeapon.ax}
@ -189,8 +197,8 @@ const WeaponModal = (props: Props) => {
<Dialog.Content className="Weapon Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }> <Dialog.Content className="Weapon Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }>
<div className="DialogHeader"> <div className="DialogHeader">
<div className="DialogTop"> <div className="DialogTop">
<Dialog.Title className="SubTitle">Modify Weapon</Dialog.Title> <Dialog.Title className="SubTitle">{t('modals.weapon.title')}</Dialog.Title>
<Dialog.Title className="DialogTitle">{props.gridWeapon.object.name.en}</Dialog.Title> <Dialog.Title className="DialogTitle">{props.gridWeapon.object.name[locale]}</Dialog.Title>
</div> </div>
<Dialog.Close className="DialogClose" asChild> <Dialog.Close className="DialogClose" asChild>
<span> <span>
@ -203,7 +211,7 @@ const WeaponModal = (props: Props) => {
{ (props.gridWeapon.object.element == 0) ? elementSelect() : '' } { (props.gridWeapon.object.element == 0) ? elementSelect() : '' }
{ ([2, 3, 17, 24].includes(props.gridWeapon.object.series)) ? keySelect() : '' } { ([2, 3, 17, 24].includes(props.gridWeapon.object.series)) ? keySelect() : '' }
{ (props.gridWeapon.object.ax > 0) ? axSelect() : '' } { (props.gridWeapon.object.ax > 0) ? axSelect() : '' }
<Button onClick={updateWeapon} disabled={props.gridWeapon.object.ax > 0 && !formValid}>Save Weapon</Button> <Button onClick={updateWeapon} disabled={props.gridWeapon.object.ax > 0 && !formValid}>{t('modals.weapon.buttons.confirm')}</Button>
</div> </div>
</Dialog.Content> </Dialog.Content>
<Dialog.Overlay className="Overlay" /> <Dialog.Overlay className="Overlay" />

View file

@ -1,4 +1,6 @@
import React from 'react' import React from 'react'
import { useRouter } from 'next/router'
import UncapIndicator from '~components/UncapIndicator' import UncapIndicator from '~components/UncapIndicator'
import WeaponLabelIcon from '~components/WeaponLabelIcon' import WeaponLabelIcon from '~components/WeaponLabelIcon'
@ -13,29 +15,29 @@ const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana'] const Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana']
const Series = ['seraphic', 'grand', 'opus', 'draconic', 'revenant', 'primal', 'beast','regalia', 'omega', 'olden_primal', 'hollowsky', 'xeno', 'astral', 'rose', 'ultima', 'bahamut', 'epic', 'ennead', 'cosmos', 'ancestral', 'superlative', 'vintage', 'class_champion', 'sephira', 'new_world_foundation'] const Series = ['seraphic', 'grand', 'opus', 'draconic', 'revenant', 'primal', 'beast','regalia', 'omega', 'olden_primal', 'hollowsky', 'xeno', 'astral', 'rose', 'ultima', 'bahamut', 'epic', 'ennead', 'cosmos', 'ancestral', 'superlative', 'vintage', 'class_champion', 'sephira', 'new_world_foundation']
class WeaponResult extends React.Component<Props> { const WeaponResult = (props: Props) => {
render() { const router = useRouter()
const weapon = this.props.data const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const weapon = props.data
return ( return (
<li className="WeaponResult" onClick={this.props.onClick}> <li className="WeaponResult" onClick={props.onClick}>
<img alt={weapon.name.en} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`} /> <img alt={weapon.name[locale]} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`} />
<div className="Info"> <div className="Info">
<h5>{weapon.name.en}</h5> <h5>{weapon.name[locale]}</h5>
<UncapIndicator <UncapIndicator
type="weapon" type="weapon"
flb={weapon.uncap.flb} flb={weapon.uncap.flb}
ulb={weapon.uncap.ulb} ulb={weapon.uncap.ulb}
special={false} special={false}
/> />
<div className="tags"> <div className="tags">
<WeaponLabelIcon labelType={Element[weapon.element]} /> <WeaponLabelIcon labelType={Element[weapon.element]} />
<WeaponLabelIcon labelType={Proficiency[weapon.proficiency]} /> <WeaponLabelIcon labelType={Proficiency[weapon.proficiency]} />
</div>
</div> </div>
</li> </div>
) </li>
} )
} }
export default WeaponResult export default WeaponResult

View file

@ -1,4 +1,6 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import classnames from 'classnames' import classnames from 'classnames'
import SearchModal from '~components/SearchModal' import SearchModal from '~components/SearchModal'
@ -22,8 +24,13 @@ interface Props {
} }
const WeaponUnit = (props: Props) => { const WeaponUnit = (props: Props) => {
const { t } = useTranslation('common')
const [imageUrl, setImageUrl] = useState('') const [imageUrl, setImageUrl] = useState('')
const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const classes = classnames({ const classes = classnames({
WeaponUnit: true, WeaponUnit: true,
'mainhand': props.unitType == 0, 'mainhand': props.unitType == 0,
@ -81,7 +88,7 @@ const WeaponUnit = (props: Props) => {
const editableImage = ( const editableImage = (
<SearchModal <SearchModal
placeholderText="Search for a weapon..." placeholderText={t('search.placeholders.weapon')}
fromPosition={props.position} fromPosition={props.position}
object="weapons" object="weapons"
send={props.updateObject}> send={props.updateObject}>
@ -108,7 +115,7 @@ const WeaponUnit = (props: Props) => {
special={false} special={false}
/> : '' /> : ''
} }
<h3 className="WeaponName">{weapon?.name.en}</h3> <h3 className="WeaponName">{weapon?.name[locale]}</h3>
</div> </div>
) )

6
next-i18next.config.js Normal file
View file

@ -0,0 +1,6 @@
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'ja']
}
}

View file

@ -1,5 +1,6 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const path = require('path') const path = require('path')
const { i18n } = require('./next-i18next.config')
module.exports = { module.exports = {
reactStrictMode: true, reactStrictMode: true,
@ -7,6 +8,7 @@ module.exports = {
prependData: '@import "variables";', prependData: '@import "variables";',
includePaths: [path.join(__dirname, 'styles')], includePaths: [path.join(__dirname, 'styles')],
}, },
i18n,
async rewrites() { async rewrites() {
return [ return [
{ {

1271
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -22,14 +22,19 @@
"@types/axios": "^0.14.0", "@types/axios": "^0.14.0",
"axios": "^0.25.0", "axios": "^0.25.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"i18next": "^21.6.13",
"i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"meyer-reset-scss": "^2.0.4", "meyer-reset-scss": "^2.0.4",
"next": "12.0.8", "next": "12.0.8",
"next-i18next": "^10.5.0",
"next-remote-watch": "^1.0.0",
"react": "17.0.2", "react": "17.0.2",
"react-cookie": "^4.1.1", "react-cookie": "^4.1.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-i18next": "^11.15.3", "react-i18next": "^11.15.5",
"react-linkify": "^1.0.0-alpha", "react-linkify": "^1.0.0-alpha",
"react-scroll": "^1.8.5", "react-scroll": "^1.8.5",
"sass": "^1.49.0", "sass": "^1.49.0",

View file

@ -1,7 +1,10 @@
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import Head from 'next/head' import Head from 'next/head'
import { useRouter } from 'next/router'
import { useCookies } from 'react-cookie' import { useCookies } from 'react-cookie'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import api from '~utils/api' import api from '~utils/api'
@ -13,6 +16,7 @@ const ProfileRoute: React.FC = () => {
const router = useRouter() const router = useRouter()
const { username } = router.query const { username } = router.query
const { t } = useTranslation('common')
const [cookies] = useCookies(['account']) const [cookies] = useCookies(['account'])
const [found, setFound] = useState(false) const [found, setFound] = useState(false)
@ -169,7 +173,7 @@ const ProfileRoute: React.FC = () => {
</GridRepCollection> </GridRepCollection>
{ (parties.length == 0) ? { (parties.length == 0) ?
<div id="NotFound"> <div id="NotFound">
<h2>{ (loading) ? 'Loading teams...' : 'No teams found' }</h2> <h2>{ (loading) ? t('teams.loading') : t('teams.not_found') }</h2>
</div> </div>
: '' } : '' }
</section> </section>
@ -177,4 +181,23 @@ const ProfileRoute: React.FC = () => {
) )
} }
export default ProfileRoute export async function getStaticPaths() {
return {
paths: [
// Object variant:
{ params: { username: 'string' } },
],
fallback: true,
}
}
export async function getStaticProps({ locale }: { locale: string }) {
return {
props: {
...(await serverSideTranslations(locale, ['common'])),
// Will be passed to the page component as props
},
}
}
export default ProfileRoute

View file

@ -1,5 +1,6 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { useCookies, CookiesProvider } from 'react-cookie' import { useCookies, CookiesProvider } from 'react-cookie'
import { appWithTranslation } from 'next-i18next'
import type { AppProps } from 'next/app' import type { AppProps } from 'next/app'
import Layout from '~components/Layout' import Layout from '~components/Layout'
@ -16,7 +17,6 @@ function MyApp({ Component, pageProps }: AppProps) {
console.log(`Logged in as user "${cookies.account.username}"`) console.log(`Logged in as user "${cookies.account.username}"`)
accountState.account.authorized = true accountState.account.authorized = true
accountState.account.language = cookies.account.language
accountState.account.user = { accountState.account.user = {
id: cookies.account.user_id, id: cookies.account.user_id,
username: cookies.account.username, username: cookies.account.username,
@ -37,4 +37,4 @@ function MyApp({ Component, pageProps }: AppProps) {
) )
} }
export default MyApp export default appWithTranslation(MyApp)

View file

@ -1,6 +1,8 @@
import React from 'react' import React from 'react'
import Party from '~components/Party' import Party from '~components/Party'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
const NewRoute = () => { const NewRoute = () => {
function callback(path: string) { function callback(path: string) {
// This is scuffed, how do we do this natively? // This is scuffed, how do we do this natively?
@ -14,4 +16,13 @@ const NewRoute = () => {
) )
} }
export async function getStaticProps({ locale }: { locale: string }) {
return {
props: {
...(await serverSideTranslations(locale, ['common'])),
// Will be passed to the page component as props
},
}
}
export default NewRoute export default NewRoute

View file

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import Party from '~components/Party' import Party from '~components/Party'
@ -30,4 +31,23 @@ const PartyRoute: React.FC = () => {
// } // }
} }
export async function getStaticPaths() {
return {
paths: [
// Object variant:
{ params: { party: 'string' } },
],
fallback: true,
}
}
export async function getStaticProps({ locale }: { locale: string }) {
return {
props: {
...(await serverSideTranslations(locale, ['common'])),
// Will be passed to the page component as props
},
}
}
export default PartyRoute export default PartyRoute

View file

@ -1,8 +1,11 @@
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import Head from 'next/head' import Head from 'next/head'
import { useRouter } from 'next/router'
import { useCookies } from 'react-cookie' import { useCookies } from 'react-cookie'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import clonedeep from 'lodash.clonedeep' import clonedeep from 'lodash.clonedeep'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import api from '~utils/api' import api from '~utils/api'
@ -12,6 +15,7 @@ import FilterBar from '~components/FilterBar'
const SavedRoute: React.FC = () => { const SavedRoute: React.FC = () => {
const router = useRouter() const router = useRouter()
const { t } = useTranslation('common')
// Cookies // Cookies
const [cookies] = useCookies(['account']) const [cookies] = useCookies(['account'])
@ -143,7 +147,7 @@ const SavedRoute: React.FC = () => {
return ( return (
<div id="Teams"> <div id="Teams">
<Head> <Head>
<title>Your saved Teams</title> <title>{t('saved.title')}</title>
<meta property="og:title" content="Your saved Teams" /> <meta property="og:title" content="Your saved Teams" />
<meta property="og:url" content="https://app.granblue.team/saved" /> <meta property="og:url" content="https://app.granblue.team/saved" />
@ -155,7 +159,7 @@ const SavedRoute: React.FC = () => {
</Head> </Head>
<FilterBar onFilter={receiveFilters} scrolled={scrolled}> <FilterBar onFilter={receiveFilters} scrolled={scrolled}>
<h1>Your saved Teams</h1> <h1>{t('saved.title')}</h1>
</FilterBar> </FilterBar>
<section> <section>
@ -182,7 +186,7 @@ const SavedRoute: React.FC = () => {
{ (parties.length == 0) ? { (parties.length == 0) ?
<div id="NotFound"> <div id="NotFound">
<h2>{ (loading) ? 'Loading saved teams...' : 'You haven&apos;t saved any teams yet' }</h2> <h2>{ (loading) ? t('saved.loading') : t('saved.not_found') }</h2>
</div> </div>
: '' } : '' }
</section> </section>
@ -190,4 +194,13 @@ const SavedRoute: React.FC = () => {
) )
} }
export async function getStaticProps({ locale }: { locale: string }) {
return {
props: {
...(await serverSideTranslations(locale, ['common'])),
// Will be passed to the page component as props
},
}
}
export default SavedRoute export default SavedRoute

View file

@ -1,9 +1,15 @@
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import Head from 'next/head' import Head from 'next/head'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useCookies } from 'react-cookie' import { useCookies } from 'react-cookie'
import { useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import clonedeep from 'lodash.clonedeep' import clonedeep from 'lodash.clonedeep'
import api from '~utils/api' import api from '~utils/api'
import GridRep from '~components/GridRep' import GridRep from '~components/GridRep'
@ -12,6 +18,7 @@ import FilterBar from '~components/FilterBar'
const TeamsRoute: React.FC = () => { const TeamsRoute: React.FC = () => {
const router = useRouter() const router = useRouter()
const { t } = useTranslation('common')
// Cookies // Cookies
const [cookies] = useCookies(['account']) const [cookies] = useCookies(['account'])
@ -148,7 +155,7 @@ const TeamsRoute: React.FC = () => {
return ( return (
<div id="Teams"> <div id="Teams">
<Head> <Head>
<title>Discover Teams</title> <title>{ t('teams.title') }</title>
<meta property="og:title" content="Discover Teams" /> <meta property="og:title" content="Discover Teams" />
<meta property="og:description" content="Find different Granblue Fantasy teams by raid, element or recency" /> <meta property="og:description" content="Find different Granblue Fantasy teams by raid, element or recency" />
@ -161,7 +168,7 @@ const TeamsRoute: React.FC = () => {
<meta name="twitter:description" content="Find different Granblue Fantasy teams by raid, element or recency" /> <meta name="twitter:description" content="Find different Granblue Fantasy teams by raid, element or recency" />
</Head> </Head>
<FilterBar onFilter={receiveFilters} scrolled={scrolled}> <FilterBar onFilter={receiveFilters} scrolled={scrolled}>
<h1>Discover Teams</h1> <h1>{t('teams.title')}</h1>
</FilterBar> </FilterBar>
<section> <section>
@ -188,7 +195,7 @@ const TeamsRoute: React.FC = () => {
{ (parties.length == 0) ? { (parties.length == 0) ?
<div id="NotFound"> <div id="NotFound">
<h2>{ (loading) ? 'Loading teams...' : 'No teams found' }</h2> <h2>{ (loading) ? t('teams.loading') : t('teams.not_found') }</h2>
</div> </div>
: '' } : '' }
</section> </section>
@ -196,4 +203,13 @@ const TeamsRoute: React.FC = () => {
) )
} }
export async function getStaticProps({ locale }: { locale: string }) {
return {
props: {
...(await serverSideTranslations(locale, ['common'])),
// Will be passed to the page component as props
},
}
}
export default TeamsRoute export default TeamsRoute

View file

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

View file

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

View file

Before

Width:  |  Height:  |  Size: 6 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -0,0 +1,182 @@
{
"ax": {
"no_skill": "No AX Skill",
"errors": {
"value_too_low": "{{name}} must be at least {{minValue}}{{suffix}}",
"value_too_high": "{{name}} cannot be greater than {{maxValue}}{{suffix}}",
"value_not_whole": "{{name}} must be a whole number",
"value_empty": "{{name}} must have a value"
}
},
"buttons": {
"copy": "Copy link",
"delete": "Delete team",
"show_info": "Edit info",
"hide_info": "Hide info",
"menu": "Menu",
"new": "New",
"wiki": "View more on gbf.wiki"
},
"elements": {
"null": "Null",
"wind": "Wind",
"fire": "Fire",
"water": "Water",
"earth": "Earth",
"dark": "Dark",
"light": "Light",
"full": {
"all": "All elements",
"null": "Null",
"wind": "Wind",
"fire": "Fire",
"water": "Water",
"earth": "Earth",
"dark": "Dark",
"light": "Light"
}
},
"recency": {
"all_time": "All time",
"last_day": "Last day",
"last_week": "Last week",
"last_month": "Last month",
"last_3_months": "Last 3 months",
"last_6_months": "Last 6 months",
"last_year": "Last year"
},
"summons": {
"main": "Main Summon",
"friend": "Friend Summon",
"summons": "Summons",
"subaura": "Sub Aura Summons"
},
"modals": {
"about": {
"title": "About"
},
"delete_team": {
"title": "Delete team",
"description": "Are you sure you want to permanently delete this team?",
"buttons": {
"confirm": "Yes, delete",
"cancel": "Nevermind"
}
},
"login": {
"title": "Log in",
"buttons": {
"confirm": "Log in"
},
"errors": {
"empty_email": "Please enter your email",
"empty_password": "Please enter your password",
"invalid_email": "That email address is not valid",
"invalid_credentials": "Your email address or password is incorrect"
},
"placeholders": {
"email": "Email address",
"password": "Password"
}
},
"settings": {
"title": "Account Settings",
"labels": {
"picture": "Picture",
"language": "Language",
"private": "Private"
},
"descriptions": {
"private": "Hide your profile and prevent your grids from showing up in collections"
},
"language": {
"english": "English",
"japanese": "Japanese"
},
"buttons": {
"confirm": "Save settings"
}
},
"signup": {
"title": "Create an account",
"buttons": {
"confirm": "Sign up"
},
"agreement": "By signing up, I agree to the <br/><2>Privacy Policy</2> and <1>Usage Guidelines</1>.",
"errors": {
"field_in_use": "This {{field}} is already in use",
"empty_email": "Please enter your email",
"invalid_email": "That email address is not valid",
"username_too_short": "Username must be at least 3 characters",
"username_too_long": "Username must be less than 20 characters",
"empty_password": "Please enter your password",
"password_contains_username": "Your password should not contain your username",
"password_too_short": "Password must be at least 8 characters",
"mismatched_passwords": "Your passwords don't match"
},
"placeholders": {
"username": "Username",
"email": "Email address",
"password": "Password",
"password_confirm": "Password (again)"
}
},
"weapon": {
"title": "Modify Weapon",
"buttons": {
"confirm": "Save weapon"
},
"subtitles": {
"element": "Element",
"ax_skills": "AX Skills",
"weapon_keys": "Weapon Keys"
}
}
},
"menu": {
"about": "About",
"guides": "Guides",
"language": "Language",
"login": "Log in",
"saved": "Saved",
"settings": "Settings",
"signup": "Sign up",
"teams": "Teams",
"logout": "Logout"
},
"party": {
"segmented_control": {
"class": "Class",
"characters": "Characters",
"weapons": "Weapons",
"summons": "Summons"
}
},
"saved": {
"title": "Your saved Teams",
"loading": "Loading saved teams...",
"not_found": "You haven't saved any teams"
},
"search": {
"errors": {
"start_typing": "Start typing the name of a {{object}}",
"min_length": "Type at least 3 characters",
"no_results": "No results found for '{{query}}'"
},
"placeholders": {
"weapon": "Search for a weapon...",
"summon": "Search for a summon...",
"character": "Search for a weapon..."
}
},
"teams": {
"title": "Discover Teams",
"loading": "Loading teams...",
"not_found": "No teams found"
},
"extra_weapons": "Additional<br/>Weapons",
"coming_soon": "Coming Soon",
"no_title": "Untitled",
"no_raid": "No raid",
"no_user": "Anonymous"
}

View file

@ -0,0 +1,184 @@
{
"ax": {
"no_skill": "EXスキルなし",
"errors": {
"value_too_low": "{{name}}は最低{{minValue}}{{suffix}}を入力してください",
"value_too_high": "{{name}}は最大{{maxValue}}を入力してください",
"value_not_whole": "{{name}}は整数でなければなりません",
"value_empty": "{{name}}を入力してください"
}
},
"buttons": {
"copy": "リンクをコピー",
"delete": "編成を削除",
"show_info": "詳細を編集",
"hide_info": "詳細を非表示",
"menu": "メニュー",
"new": "作成",
"wiki": "gbf.wikiで詳しく見る"
},
"elements": {
"null": "無",
"wind": "風",
"fire": "火",
"water": "水",
"earth": "土",
"dark": "闇",
"light": "光",
"full": {
"all": "全属性",
"null": "無属性",
"wind": "風属性",
"fire": "火属性",
"water": "水属性",
"earth": "土属性",
"dark": "闇属性",
"light": "光属性"
}
},
"recency": {
"all_time": "全ての期間",
"last_day": "1日",
"last_week": "7日",
"last_month": "1ヶ月",
"last_3_months": "3ヶ月",
"last_6_months": "6ヶ月",
"last_year": "1年"
},
"summons": {
"main": "メイン",
"friend": "フレンド",
"summons": "召喚石",
"subaura": "サブ加護召喚石"
},
"modals": {
"about": {
"title": "このサイトについて"
},
"delete_team": {
"title": "編成を削除しますか",
"description": "編成を削除する操作は取り消せません。",
"buttons": {
"confirm": "削除",
"cancel": "キャンセル"
}
},
"login": {
"title": "ログイン",
"buttons": {
"confirm": "ログイン"
},
"errors": {
"empty_email": "メールアドレスを入力して下さい",
"empty_password": "パスワードを入力して下さい",
"invalid_email": "メールアドレスは有効ではありません",
"invalid_credentials": "パスワードまたはメールアドレスが違います"
},
"placeholders": {
"email": "メールアドレス",
"password": "パスワード"
}
},
"settings": {
"title": "アカウント設定",
"labels": {
"picture": "プロフィール画像",
"language": "言語",
"private": "プライベート"
},
"descriptions": {
"private": "プロフィールを隠し、編成をコレクションに表示されないようにします"
},
"language": {
"english": "英語",
"japanese": "日本語"
},
"buttons": {
"confirm": "設定を保存する"
}
},
"signup": {
"title": "アカウント登録",
"buttons": {
"confirm": "登録する"
},
"agreement": "続行することで<1>利用規約</1>に同意し、<br/><1>プライバシーポリシー</1>を読んだものとみなされます。",
"errors": {
"field_in_use": "入力された{{field}}は既に登録済みです",
"empty_email": "メールアドレスを入力して下さい",
"invalid_email": "メールアドレスは有効ではありません",
"username_too_short": "ユーザーネームは3文字以上で入力してください",
"username_too_long": "ユーザーネームは20文字以内で入力してください",
"empty_password": "パスワードを入力して下さい",
"password_contains_username": "パスワードにはユーザー名を含めないでください",
"password_too_short": "パスワードは8文字以上で入力してください",
"mismatched_passwords": "パスワードとパスワード確認を確かめてください",
"invalid_credentials": "パスワードまたはメールアドレスが違います"
},
"placeholders": {
"username": "ユーザー名",
"email": "メールアドレス",
"password": "パスワード",
"password_confirm": "パスワード確認"
}
},
"weapon": {
"title": "武器変更",
"buttons": {
"confirm": "武器を変更する"
},
"subtitles": {
"element": "属性",
"ax_skills": "EXスキル",
"weapon_keys": "武器スキル"
}
}
},
"menu": {
"about": "このサイトについて",
"guides": "攻略",
"language": "言語",
"login": "ログイン",
"saved": "保存した編成",
"settings": "アカウント設定",
"signup": "登録",
"teams": "編成一覧",
"logout": "ログアウト"
},
"party": {
"segmented_control": {
"class": "ジョブ",
"characters": "キャラ",
"weapons": "武器",
"summons": "召喚石"
}
},
"saved": {
"title": "保存した編成",
"loading": "ロード中...",
"not_found": "編成はまだ保存していません"
},
"search": {
"errors": {
"start_typing": "{{object}}名を入力してください",
"min_length": "3文字以上を入力してください",
"no_results": "'{{query}}'の検索結果が見つかりませんでした"
},
"placeholders": {
"weapon": "武器を検索...",
"summon": "召喚石を検索...",
"character": "キャラを検索..."
}
},
"teams": {
"title": "編成一覧",
"loading": "ロード中...",
"not_found": "編成は見つかりませんでした"
},
"extra_weapons": "Additional<br/>Weapons",
"coming_soon": "開発中",
"no_title": "無題",
"no_raid": "マルチなし",
"no_user": "無名"
}

3
types/AxSkill.d.ts vendored
View file

@ -1,7 +1,8 @@
interface AxSkill { interface AxSkill {
name: { name: {
[key: string]: string
en: string, en: string,
jp: string ja: string
}, },
id: number, id: number,
minValue: number, minValue: number,

View file

@ -8,8 +8,9 @@ interface Character {
gender: number gender: number
max_level: number max_level: number
name: { name: {
[key: string]: string
en: string en: string
jp: string ja: string
} }
hp: { hp: {
min_hp: number min_hp: number

3
types/Raid.d.ts vendored
View file

@ -1,8 +1,9 @@
interface Raid { interface Raid {
id: string id: string
name: { name: {
[key: string]: string
en: string en: string
jp: string ja: string
} }
level: number level: number
group: number group: number

3
types/Summon.d.ts vendored
View file

@ -6,8 +6,9 @@ interface Summon {
element: number element: number
max_level: number max_level: number
name: { name: {
[key: string]: string
en: string en: string
jp: string ja: string
} }
hp: { hp: {
min_hp: number min_hp: number

3
types/Weapon.d.ts vendored
View file

@ -10,8 +10,9 @@ interface Weapon {
series: number series: number
ax: number ax: number
name: { name: {
[key: string]: string
en: string en: string
jp: string ja: string
} }
hp: { hp: {
min_hp: number min_hp: number

View file

@ -1,8 +1,9 @@
interface WeaponKey { interface WeaponKey {
id: string id: string
name: { name: {
[key: string]: string
en: string, en: string,
jp: string ja: string
} }
series: integer series: integer
slot: integer slot: integer

View file

@ -5,7 +5,6 @@ interface AccountState {
account: { account: {
authorized: boolean authorized: boolean
language: 'en' | 'jp'
user: { user: {
id: string id: string
username: string username: string
@ -18,7 +17,6 @@ interface AccountState {
export const initialAccountState: AccountState = { export const initialAccountState: AccountState = {
account: { account: {
authorized: false, authorized: false,
language: 'en',
user: undefined user: undefined
} }
} }

View file

@ -3,7 +3,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "ATK", "en": "ATK",
"jp": "攻撃" "ja": "攻撃"
}, },
id: 0, id: 0,
minValue: 1, minValue: 1,
@ -13,7 +13,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "C.A. DMG", "en": "C.A. DMG",
"jp": "奥義ダメ" "ja": "奥義ダメ"
}, },
id: 3, id: 3,
minValue: 2, minValue: 2,
@ -23,7 +23,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Double Attack Rate", "en": "Double Attack Rate",
"jp": "DA確率" "ja": "DA確率"
}, },
id: 5, id: 5,
minValue: 1, minValue: 1,
@ -33,7 +33,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Triple Attack Rate", "en": "Triple Attack Rate",
"jp": "TA確率" "ja": "TA確率"
}, },
id: 6, id: 6,
minValue: 1, minValue: 1,
@ -43,7 +43,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Skill DMG Cap", "en": "Skill DMG Cap",
"jp": "アビ上限" "ja": "アビ上限"
}, },
id: 7, id: 7,
minValue: 1, minValue: 1,
@ -55,7 +55,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "DEF", "en": "DEF",
"jp": "防御" "ja": "防御"
}, },
id: 1, id: 1,
minValue: 1, minValue: 1,
@ -65,7 +65,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "HP", "en": "HP",
"jp": "HP" "ja": "HP"
}, },
id: 2, id: 2,
minValue: 1, minValue: 1,
@ -75,7 +75,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Debuff Resistance", "en": "Debuff Resistance",
"jp": "弱体耐性" "ja": "弱体耐性"
}, },
id: 9, id: 9,
minValue: 1, minValue: 1,
@ -85,7 +85,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Healing", "en": "Healing",
"jp": "回復性能" "ja": "回復性能"
}, },
id: 10, id: 10,
minValue: 2, minValue: 2,
@ -95,7 +95,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Enmity", "en": "Enmity",
"jp": "背水" "ja": "背水"
}, },
id: 11, id: 11,
minValue: 1, minValue: 1,
@ -106,7 +106,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "HP", "en": "HP",
"jp": "HP" "ja": "HP"
}, },
id: 2, id: 2,
minValue: 1, minValue: 1,
@ -116,7 +116,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "DEF", "en": "DEF",
"jp": "防御" "ja": "防御"
}, },
id: 1, id: 1,
minValue: 1, minValue: 1,
@ -126,7 +126,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Debuff Resistance", "en": "Debuff Resistance",
"jp": "弱体耐性" "ja": "弱体耐性"
}, },
id: 9, id: 9,
minValue: 1, minValue: 1,
@ -136,7 +136,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Healing", "en": "Healing",
"jp": "回復性能" "ja": "回復性能"
}, },
id: 10, id: 10,
minValue: 2, minValue: 2,
@ -146,7 +146,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Stamina", "en": "Stamina",
"jp": "渾身" "ja": "渾身"
}, },
id: 12, id: 12,
minValue: 1, minValue: 1,
@ -157,7 +157,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "C.A. DMG", "en": "C.A. DMG",
"jp": "奥義ダメ" "ja": "奥義ダメ"
}, },
id: 3, id: 3,
minValue: 2, minValue: 2,
@ -167,7 +167,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "ATK", "en": "ATK",
"jp": "攻撃" "ja": "攻撃"
}, },
id: 0, id: 0,
minValue: 1, minValue: 1,
@ -177,7 +177,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Elemental ATK", "en": "Elemental ATK",
"jp": "全属性攻撃力" "ja": "全属性攻撃力"
}, },
id: 13, id: 13,
minValue: 1, minValue: 1,
@ -187,7 +187,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "C.A. DMG Cap", "en": "C.A. DMG Cap",
"jp": "奥義上限" "ja": "奥義上限"
}, },
id: 8, id: 8,
minValue: 1, minValue: 1,
@ -197,7 +197,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Stamina", "en": "Stamina",
"jp": "渾身" "ja": "渾身"
}, },
id: 12, id: 12,
minValue: 1, minValue: 1,
@ -208,7 +208,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Multiattack Rate", "en": "Multiattack Rate",
"jp": "連撃率" "ja": "連撃率"
}, },
id: 4, id: 4,
minValue: 1, minValue: 1,
@ -218,7 +218,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "C.A. DMG", "en": "C.A. DMG",
"jp": "奥義ダメ" "ja": "奥義ダメ"
}, },
id: 3, id: 3,
minValue: 2, minValue: 2,
@ -228,7 +228,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Elemental ATK", "en": "Elemental ATK",
"jp": "全属性攻撃力" "ja": "全属性攻撃力"
}, },
id: 13, id: 13,
minValue: 1, minValue: 1,
@ -238,7 +238,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Double Attack Rate", "en": "Double Attack Rate",
"jp": "DA確率" "ja": "DA確率"
}, },
id: 5, id: 5,
minValue: 1, minValue: 1,
@ -248,7 +248,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Triple Attack Rate", "en": "Triple Attack Rate",
"jp": "TA確率" "ja": "TA確率"
}, },
id: 6, id: 6,
minValue: 1, minValue: 1,
@ -261,7 +261,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "ATK", "en": "ATK",
"jp": "攻撃" "ja": "攻撃"
}, },
id: 0, id: 0,
minValue: 1, minValue: 1,
@ -271,7 +271,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "C.A. DMG", "en": "C.A. DMG",
"jp": "奥義ダメ" "ja": "奥義ダメ"
}, },
id: 3, id: 3,
minValue: 2, minValue: 2,
@ -281,7 +281,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Multiattack Rate", "en": "Multiattack Rate",
"jp": "連撃確率" "ja": "連撃確率"
}, },
id: 4, id: 4,
minValue: 1.5, minValue: 1.5,
@ -291,7 +291,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Normal ATK DMG Cap", "en": "Normal ATK DMG Cap",
"jp": "通常ダメ上限" "ja": "通常ダメ上限"
}, },
id: 14, id: 14,
minValue: 0.5, minValue: 0.5,
@ -301,7 +301,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Supplemental Skill DMG", "en": "Supplemental Skill DMG",
"jp": "アビ与ダメ上昇" "ja": "アビ与ダメ上昇"
}, },
id: 15, id: 15,
minValue: 1, minValue: 1,
@ -312,7 +312,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "DEF", "en": "DEF",
"jp": "防御" "ja": "防御"
}, },
id: 1, id: 1,
minValue: 1, minValue: 1,
@ -322,7 +322,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Elemental DMG Reduction", "en": "Elemental DMG Reduction",
"jp": "属性ダメ軽減" "ja": "属性ダメ軽減"
}, },
id: 17, id: 17,
minValue: 1, minValue: 1,
@ -332,7 +332,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Debuff Resistance", "en": "Debuff Resistance",
"jp": "弱体耐性" "ja": "弱体耐性"
}, },
id: 9, id: 9,
minValue: 1, minValue: 1,
@ -342,7 +342,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Healing", "en": "Healing",
"jp": "回復性能" "ja": "回復性能"
}, },
id: 10, id: 10,
minValue: 2, minValue: 2,
@ -352,7 +352,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Enmity", "en": "Enmity",
"jp": "背水" "ja": "背水"
}, },
id: 11, id: 11,
minValue: 1, minValue: 1,
@ -363,7 +363,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "HP", "en": "HP",
"jp": "HP" "ja": "HP"
}, },
id: 2, id: 2,
minValue: 1, minValue: 1,
@ -373,7 +373,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Elemental DMG Reduction", "en": "Elemental DMG Reduction",
"jp": "属性ダメ軽減" "ja": "属性ダメ軽減"
}, },
id: 17, id: 17,
minValue: 1, minValue: 1,
@ -383,7 +383,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Debuff Resistance", "en": "Debuff Resistance",
"jp": "弱体耐性" "ja": "弱体耐性"
}, },
id: 9, id: 9,
minValue: 1, minValue: 1,
@ -393,7 +393,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Healing", "en": "Healing",
"jp": "回復性能" "ja": "回復性能"
}, },
id: 10, id: 10,
minValue: 2, minValue: 2,
@ -403,7 +403,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Stamina", "en": "Stamina",
"jp": "渾身" "ja": "渾身"
}, },
id: 12, id: 12,
minValue: 1, minValue: 1,
@ -414,7 +414,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "C.A. DMG", "en": "C.A. DMG",
"jp": "奥義ダメ" "ja": "奥義ダメ"
}, },
id: 3, id: 3,
minValue: 2, minValue: 2,
@ -424,7 +424,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Multiattack Rate", "en": "Multiattack Rate",
"jp": "連撃率" "ja": "連撃率"
}, },
id: 4, id: 4,
minValue: 1.5, minValue: 1.5,
@ -434,7 +434,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Supplemental Skill DMG", "en": "Supplemental Skill DMG",
"jp": "アビ与ダメ上昇" "ja": "アビ与ダメ上昇"
}, },
id: 15, id: 15,
minValue: 1, minValue: 1,
@ -443,7 +443,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Supplemental C.A. DMG", "en": "Supplemental C.A. DMG",
"jp": "奥義与ダメ上昇" "ja": "奥義与ダメ上昇"
}, },
id: 16, id: 16,
minValue: 1, minValue: 1,
@ -452,7 +452,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Stamina", "en": "Stamina",
"jp": "渾身" "ja": "渾身"
}, },
id: 12, id: 12,
minValue: 1, minValue: 1,
@ -463,7 +463,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Multiattack Rate", "en": "Multiattack Rate",
"jp": "連撃率" "ja": "連撃率"
}, },
id: 4, id: 4,
minValue: 1, minValue: 1,
@ -473,7 +473,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Supplemental C.A. DMG", "en": "Supplemental C.A. DMG",
"jp": "奥義与ダメ上昇" "ja": "奥義与ダメ上昇"
}, },
id: 16, id: 16,
minValue: 1, minValue: 1,
@ -482,7 +482,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Normal ATK DMG Cap", "en": "Normal ATK DMG Cap",
"jp": "通常ダメ上限" "ja": "通常ダメ上限"
}, },
id: 14, id: 14,
minValue: 0.5, minValue: 0.5,
@ -492,7 +492,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Stamina", "en": "Stamina",
"jp": "渾身" "ja": "渾身"
}, },
id: 12, id: 12,
minValue: 1, minValue: 1,
@ -501,7 +501,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Enmity", "en": "Enmity",
"jp": "背水" "ja": "背水"
}, },
id: 11, id: 11,
minValue: 1, minValue: 1,
@ -513,7 +513,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "ATK", "en": "ATK",
"jp": "攻撃" "ja": "攻撃"
}, },
id: 0, id: 0,
minValue: 1, minValue: 1,
@ -523,7 +523,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "C.A. DMG", "en": "C.A. DMG",
"jp": "奥義ダメ" "ja": "奥義ダメ"
}, },
id: 3, id: 3,
minValue: 2, minValue: 2,
@ -533,7 +533,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Double Attack Rate", "en": "Double Attack Rate",
"jp": "DA確率" "ja": "DA確率"
}, },
id: 5, id: 5,
minValue: 1, minValue: 1,
@ -543,7 +543,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Triple Attack Rate", "en": "Triple Attack Rate",
"jp": "TA確率" "ja": "TA確率"
}, },
id: 6, id: 6,
minValue: 1, minValue: 1,
@ -553,7 +553,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Skill DMG Cap", "en": "Skill DMG Cap",
"jp": "アビ上限" "ja": "アビ上限"
}, },
id: 7, id: 7,
minValue: 1, minValue: 1,
@ -565,7 +565,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "DEF", "en": "DEF",
"jp": "防御" "ja": "防御"
}, },
id: 1, id: 1,
minValue: 1, minValue: 1,
@ -575,7 +575,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "HP", "en": "HP",
"jp": "HP" "ja": "HP"
}, },
id: 2, id: 2,
minValue: 1, minValue: 1,
@ -585,7 +585,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Debuff Resistance", "en": "Debuff Resistance",
"jp": "弱体耐性" "ja": "弱体耐性"
}, },
id: 9, id: 9,
minValue: 1, minValue: 1,
@ -595,7 +595,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Healing", "en": "Healing",
"jp": "回復性能" "ja": "回復性能"
}, },
id: 10, id: 10,
minValue: 2, minValue: 2,
@ -605,7 +605,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Enmity", "en": "Enmity",
"jp": "背水" "ja": "背水"
}, },
id: 11, id: 11,
minValue: 1, minValue: 1,
@ -616,7 +616,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "HP", "en": "HP",
"jp": "HP" "ja": "HP"
}, },
id: 2, id: 2,
minValue: 1, minValue: 1,
@ -626,7 +626,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "DEF", "en": "DEF",
"jp": "防御" "ja": "防御"
}, },
id: 1, id: 1,
minValue: 1, minValue: 1,
@ -636,7 +636,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Debuff Resistance", "en": "Debuff Resistance",
"jp": "弱体耐性" "ja": "弱体耐性"
}, },
id: 9, id: 9,
minValue: 1, minValue: 1,
@ -646,7 +646,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Healing", "en": "Healing",
"jp": "回復性能" "ja": "回復性能"
}, },
id: 10, id: 10,
minValue: 2, minValue: 2,
@ -656,7 +656,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Stamina", "en": "Stamina",
"jp": "渾身" "ja": "渾身"
}, },
id: 12, id: 12,
minValue: 1, minValue: 1,
@ -667,7 +667,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "C.A. DMG", "en": "C.A. DMG",
"jp": "奥義ダメ" "ja": "奥義ダメ"
}, },
id: 3, id: 3,
minValue: 2, minValue: 2,
@ -677,7 +677,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "ATK", "en": "ATK",
"jp": "攻撃" "ja": "攻撃"
}, },
id: 0, id: 0,
minValue: 1, minValue: 1,
@ -687,7 +687,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Elemental ATK", "en": "Elemental ATK",
"jp": "全属性攻撃力" "ja": "全属性攻撃力"
}, },
id: 13, id: 13,
minValue: 1, minValue: 1,
@ -697,7 +697,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "C.A. DMG Cap", "en": "C.A. DMG Cap",
"jp": "奥義上限" "ja": "奥義上限"
}, },
id: 8, id: 8,
minValue: 1, minValue: 1,
@ -707,7 +707,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Stamina", "en": "Stamina",
"jp": "渾身" "ja": "渾身"
}, },
id: 12, id: 12,
minValue: 1, minValue: 1,
@ -718,7 +718,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Multiattack Rate", "en": "Multiattack Rate",
"jp": "連撃率" "ja": "連撃率"
}, },
id: 4, id: 4,
minValue: 1, minValue: 1,
@ -728,7 +728,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "C.A. DMG", "en": "C.A. DMG",
"jp": "奥義ダメ" "ja": "奥義ダメ"
}, },
id: 3, id: 3,
minValue: 2, minValue: 2,
@ -738,7 +738,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Elemental ATK", "en": "Elemental ATK",
"jp": "全属性攻撃力" "ja": "全属性攻撃力"
}, },
id: 13, id: 13,
minValue: 1, minValue: 1,
@ -748,7 +748,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Double Attack Rate", "en": "Double Attack Rate",
"jp": "DA確率" "ja": "DA確率"
}, },
id: 5, id: 5,
minValue: 1, minValue: 1,
@ -758,7 +758,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Triple Attack Rate", "en": "Triple Attack Rate",
"jp": "TA確率" "ja": "TA確率"
}, },
id: 6, id: 6,
minValue: 1, minValue: 1,
@ -770,7 +770,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "EXP Up", "en": "EXP Up",
"jp": "EXP UP" "ja": "EXP UP"
}, },
id: 18, id: 18,
minValue: 5, minValue: 5,
@ -780,7 +780,7 @@ export const axData: AxSkill[][] = [
{ {
name: { name: {
"en": "Rupies", "en": "Rupies",
"jp": "獲得ルピ" "ja": "獲得ルピ"
}, },
id: 19, id: 19,
minValue: 10, minValue: 10,

View file

@ -1,7 +1,8 @@
interface Picture { interface Picture {
name: { name: {
[key: string]: string
en: string en: string
jp: string ja: string
} }
filename: string filename: string
element: string element: string
@ -11,7 +12,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Gran 2019", en: "Gran 2019",
jp: "グラン" ja: "グラン"
}, },
filename: "gran_19", filename: "gran_19",
element: "water" element: "water"
@ -19,7 +20,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Djeeta 2019", en: "Djeeta 2019",
jp: "ジータ" ja: "ジータ"
}, },
filename: "djeeta_19", filename: "djeeta_19",
element: "fire" element: "fire"
@ -27,7 +28,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Gran 2020", en: "Gran 2020",
jp: "グラン" ja: "グラン"
}, },
filename: "gran_20", filename: "gran_20",
element: "water" element: "water"
@ -35,7 +36,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Djeeta 2020", en: "Djeeta 2020",
jp: "ジータ" ja: "ジータ"
}, },
filename: "djeeta_20", filename: "djeeta_20",
element: "fire" element: "fire"
@ -43,7 +44,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Gran - Farer of the Skies", en: "Gran - Farer of the Skies",
jp: "空駆ける新鋭 グランver" ja: "空駆ける新鋭 グランver"
}, },
filename: "gran", filename: "gran",
element: "water" element: "water"
@ -51,7 +52,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Djeeta - Farer of the Skies", en: "Djeeta - Farer of the Skies",
jp: "空駆ける新鋭 ジータver" ja: "空駆ける新鋭 ジータver"
}, },
filename: "djeeta", filename: "djeeta",
element: "fire" element: "fire"
@ -59,7 +60,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Cassius", en: "Cassius",
jp: "カシウス" ja: "カシウス"
}, },
filename: "cassius", filename: "cassius",
element: "dark" element: "dark"
@ -67,7 +68,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Percival", en: "Percival",
jp: "パーシヴァル" ja: "パーシヴァル"
}, },
filename: "percival", filename: "percival",
element: "fire" element: "fire"
@ -75,7 +76,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Vane", en: "Vane",
jp: "ヴェイン" ja: "ヴェイン"
}, },
filename: "vane", filename: "vane",
element: "water" element: "water"
@ -83,7 +84,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Heles", en: "Heles",
jp: "ヘルエス" ja: "ヘルエス"
}, },
filename: "heles", filename: "heles",
element: "fire" element: "fire"
@ -91,7 +92,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Lunalu", en: "Lunalu",
jp: "ルナール" ja: "ルナール"
}, },
filename: "lunalu", filename: "lunalu",
element: "dark" element: "dark"
@ -99,7 +100,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Catura", en: "Catura",
jp: "シャトラ" ja: "シャトラ"
}, },
filename: "catura", filename: "catura",
element: "wind" element: "wind"
@ -107,7 +108,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Yuisis", en: "Yuisis",
jp: "ユイシス" ja: "ユイシス"
}, },
filename: "yuisis", filename: "yuisis",
element: "wind" element: "wind"
@ -115,7 +116,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Meg", en: "Meg",
jp: "メグ" ja: "メグ"
}, },
filename: "meg", filename: "meg",
element: "dark" element: "dark"
@ -123,7 +124,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Seofon", en: "Seofon",
jp: "シエテ" ja: "シエテ"
}, },
filename: "seofon", filename: "seofon",
element: "wind" element: "wind"
@ -131,7 +132,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Quatre", en: "Quatre",
jp: "カトル" ja: "カトル"
}, },
filename: "quatre", filename: "quatre",
element: "water" element: "water"
@ -139,7 +140,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Tien", en: "Tien",
jp: "エッセル" ja: "エッセル"
}, },
filename: "tien", filename: "tien",
element: "fire" element: "fire"
@ -147,7 +148,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Seox", en: "Seox",
jp: "シス" ja: "シス"
}, },
filename: "seox", filename: "seox",
element: "dark" element: "dark"
@ -155,7 +156,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Aoidos", en: "Aoidos",
jp: "アオイドス" ja: "アオイドス"
}, },
filename: "aoidos", filename: "aoidos",
element: "fire" element: "fire"
@ -163,7 +164,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Sandalphon", en: "Sandalphon",
jp: "サンダルフォン" ja: "サンダルフォン"
}, },
filename: "sandalphon", filename: "sandalphon",
element: "light" element: "light"
@ -171,7 +172,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Vikala", en: "Vikala",
jp: "ビカラ" ja: "ビカラ"
}, },
filename: "vikala", filename: "vikala",
element: "dark" element: "dark"
@ -179,7 +180,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Belial", en: "Belial",
jp: "ベリアル" ja: "ベリアル"
}, },
filename: "belial", filename: "belial",
element: "dark" element: "dark"
@ -187,7 +188,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Zeta", en: "Zeta",
jp: "ゼタ" ja: "ゼタ"
}, },
filename: "zeta", filename: "zeta",
element: "fire" element: "fire"
@ -195,7 +196,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Beatrix", en: "Beatrix",
jp: "ベアトリックス" ja: "ベアトリックス"
}, },
filename: "beatrix", filename: "beatrix",
element: "earth" element: "earth"
@ -203,7 +204,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Yuel", en: "Yuel",
jp: "ユエル" ja: "ユエル"
}, },
filename: "yuel", filename: "yuel",
element: "fire" element: "fire"
@ -211,7 +212,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Societte", en: "Societte",
jp: "ソシエ" ja: "ソシエ"
}, },
filename: "societte", filename: "societte",
element: "water" element: "water"
@ -219,7 +220,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Kumbhira", en: "Kumbhira",
jp: "クビラ" ja: "クビラ"
}, },
filename: "kumbhira", filename: "kumbhira",
element: "light" element: "light"
@ -227,7 +228,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Narmaya", en: "Narmaya",
jp: "ナルメア" ja: "ナルメア"
}, },
filename: "narmaya", filename: "narmaya",
element: "dark" element: "dark"
@ -235,7 +236,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Siegfried", en: "Siegfried",
jp: "ジークフリード" ja: "ジークフリード"
}, },
filename: "siegfried", filename: "siegfried",
element: "earth" element: "earth"
@ -243,7 +244,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Naoise", en: "Naoise",
jp: "ノイシュ" ja: "ノイシュ"
}, },
filename: "naoise", filename: "naoise",
element: "light" element: "light"
@ -251,7 +252,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Scathacha", en: "Scathacha",
jp: "スカーサハ" ja: "スカーサハ"
}, },
filename: "scathacha", filename: "scathacha",
element: "wind" element: "wind"
@ -259,7 +260,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Seruel", en: "Seruel",
jp: "セルエル" ja: "セルエル"
}, },
filename: "seruel", filename: "seruel",
element: "light" element: "light"
@ -267,7 +268,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Shiva", en: "Shiva",
jp: "シヴァ" ja: "シヴァ"
}, },
filename: "shiva", filename: "shiva",
element: "fire" element: "fire"
@ -275,7 +276,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Europa", en: "Europa",
jp: "エウロペ" ja: "エウロペ"
}, },
filename: "europa", filename: "europa",
element: "water" element: "water"
@ -283,7 +284,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Grimnir", en: "Grimnir",
jp: "グリームニル" ja: "グリームニル"
}, },
filename: "grimnir", filename: "grimnir",
element: "wind" element: "wind"
@ -291,7 +292,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Alexiel", en: "Alexiel",
jp: "ブローディア" ja: "ブローディア"
}, },
filename: "alexiel", filename: "alexiel",
element: "earth" element: "earth"
@ -299,7 +300,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Sierokarte", en: "Sierokarte",
jp: "シェロカルテ" ja: "シェロカルテ"
}, },
filename: "siero", filename: "siero",
element: "wind" element: "wind"
@ -307,7 +308,7 @@ export const pictureData: Picture[] = [
{ {
name: { name: {
en: "Vajra", en: "Vajra",
jp: "ヴァジラ" ja: "ヴァジラ"
}, },
filename: "vajra", filename: "vajra",
element: "water" element: "water"