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

View file

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

View file

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

View file

@ -1,4 +1,7 @@
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import classNames from 'classnames'
import { axData } from '~utils/axData'
@ -19,6 +22,10 @@ interface 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
const [errors, setErrors] = useState<ErrorMap>({
axValue1: '',
@ -84,7 +91,7 @@ const AXSelect = (props: Props) => {
if (modifierSet == 0) {
axOptionElements = axOptions.map((ax, i) => {
return (
<option key={i} value={ax.id}>{ax.name.en}</option>
<option key={i} value={ax.id}>{ax.name[locale]}</option>
)
})
} else {
@ -103,14 +110,14 @@ const AXSelect = (props: Props) => {
const secondaryAxOptions = primarySkill.secondary
axOptionElements = secondaryAxOptions.map((ax, i) => {
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
}
@ -156,11 +163,19 @@ const AXSelect = (props: Props) => {
let newErrors = {...errors}
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) {
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) {
newErrors.axValue1 = `${primaryAxSkill.name.en} must have a value`
newErrors.axValue1 = t('ax.errors.value_empty', { name: primaryAxSkill.name[locale] })
} else {
newErrors.axValue1 = ''
}
@ -179,13 +194,21 @@ const AXSelect = (props: Props) => {
if (secondaryAxSkill) {
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) {
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) {
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) {
newErrors.axValue1 = `${primaryAxSkill.name.en} must have a value`
newErrors.axValue1 = t('ax.errors.value_empty', { name: primaryAxSkill.name[locale] })
} else {
newErrors.axValue2 = ''
}
@ -224,7 +247,7 @@ const AXSelect = (props: Props) => {
<div className="AXSet">
<div className="fields">
<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>
<p className={primaryErrorClasses}>{errors.axValue1}</p>
</div>
@ -232,7 +255,7 @@ const AXSelect = (props: Props) => {
<div className={secondarySetClasses}>
<div className="fields">
<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>
<p className={secondaryErrorClasses}>{errors.axValue2}</p>
</div>

View file

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

View file

@ -1,11 +1,12 @@
import React from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import * as HoverCard from '@radix-ui/react-hover-card'
import WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from '~components/UncapIndicator'
import { axData } from '~utils/axData'
import './index.scss'
interface Props {
@ -21,6 +22,10 @@ interface KeyNames {
}
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 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">
<div className="top">
<div className="title">
<h4>{ props.gridCharacter.object.name.en }</h4>
<img alt={props.gridCharacter.object.name.en} src={characterImage()} />
<h4>{ props.gridCharacter.object.name[locale] }</h4>
<img alt={props.gridCharacter.object.name[locale]} src={characterImage()} />
</div>
<div className="subInfo">
<div className="icons">
@ -76,7 +81,7 @@ const CharacterHovercard = (props: Props) => {
</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.Content>
</HoverCard.Root>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,8 @@
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next'
import classNames from 'classnames'
import { accountState } from '~utils/accountState'
@ -30,6 +32,10 @@ const GridRep = (props: Props) => {
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 [weapons, setWeapons] = useState<GridArray<Weapon>>({})
@ -66,12 +72,12 @@ const GridRep = (props: Props) => {
function generateMainhandImage() {
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) {
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() {
@ -96,22 +102,22 @@ const GridRep = (props: Props) => {
const 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={raidClass}>{ (props.raid) ? props.raid.name.en : 'No raid set' }</div>
<time className="last-updated" dateTime={props.createdAt.toISOString()}>{formatTimeAgo(props.createdAt, 'en-us')}</time>
<div className={raidClass}>{ (props.raid) ? props.raid.name[locale] : t('no_raid') }</div>
<time className="last-updated" dateTime={props.createdAt.toISOString()}>{formatTimeAgo(props.createdAt, locale)}</time>
</div>
</div>
)
const detailsWithUsername = (
<div className="Details">
<div className="top">
<div className="info">
<h2 className={titleClass} onClick={navigate}>{ (props.name) ? props.name : 'Untitled' }</h2>
<div className={raidClass}>{ (props.raid) ? props.raid.name.en : 'No raid set' }</div>
<h2 className={titleClass} onClick={navigate}>{ (props.name) ? props.name : t('no_title') }</h2>
<div className={raidClass}>{ (props.raid) ? props.raid.name[locale] : t('no_raid') }</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
active={props.favorited}
icon="save"
@ -122,9 +128,9 @@ const GridRep = (props: Props) => {
<div className="bottom">
<div className={userClass}>
{ userImage() }
{ (props.user) ? props.user.username : 'Anonymous' }
{ (props.user) ? props.user.username : t('no_user') }
</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>
)

View file

@ -26,6 +26,73 @@
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 {
color: $grey-40;
}
@ -54,7 +121,7 @@
img {
$diameter: 32px;
border-radius: $diameter / 2;
border-radius: calc($diameter / 2);
height: $diameter;
width: $diameter;
}

View file

@ -1,6 +1,10 @@
import React from 'react'
import Link from 'next/link'
import React, { useEffect, useState } from 'react'
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 AccountModal from '~components/AccountModal'
@ -16,8 +20,27 @@ interface Props {
}
const HeaderMenu = (props: Props) => {
const router = useRouter()
const { t } = useTranslation('common')
const [accountCookies] = useCookies(['account'])
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() {
return (
@ -35,22 +58,22 @@ const HeaderMenu = (props: Props) => {
/profile/${userCookies.user.picture}@2x.png 2x`}
src={`/profile/${userCookies.user.picture}.png`}
/>
</div
></Link>
</div>
</Link>
</li>
<li className="MenuItem">
<Link href={`/saved` || ''}>Saved</Link>
<Link href={`/saved` || ''}>{t('menu.saved')}</Link>
</li>
</div>
<div className="MenuGroup">
<li className="MenuItem">
<Link href='/teams'>Teams</Link>
<Link href='/teams'>{t('menu.teams')}</Link>
</li>
<li className="MenuItem disabled">
<div>
<span>Guides</span>
<i className="tag">Coming Soon</i>
<span>{t('menu.guides')}</span>
<i className="tag">{t('coming_soon')}</i>
</div>
</li>
</div>
@ -58,7 +81,7 @@ const HeaderMenu = (props: Props) => {
<AboutModal />
<AccountModal />
<li className="MenuItem" onClick={props.logout}>
<span>Logout</span>
<span>{t('menu.logout')}</span>
</li>
</div>
</ul>
@ -70,20 +93,30 @@ const HeaderMenu = (props: Props) => {
return (
<ul className="Menu unauth">
<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 className="MenuGroup">
<li className="MenuItem">
<Link href='/teams'>Teams</Link>
</li>
<li className="MenuItem">
<Link href='/teams'>{t('menu.teams')}</Link>
</li>
<li className="MenuItem disabled">
<div>
<span>Guides</span>
<i className="tag">Coming Soon</i>
</div>
</li>
</div>
<li className="MenuItem disabled">
<div>
<span>{t('menu.guides')}</span>
<i className="tag">{t('coming_soon')}</i>
</div>
</li>
</div>
<div className="MenuGroup">
<AboutModal />
</div>
<div className="MenuGroup">
<LoginModal />
<SignupModal />

View file

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

View file

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

View file

@ -1,4 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import { appState } from '~utils/appState'
import api from '~utils/api'
@ -14,6 +16,10 @@ interface Props {
}
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 raidGroups = [
@ -37,7 +43,7 @@ const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFiel
id: '0',
name: {
en: 'All raids',
jp: '全て'
ja: '全てのマルチ'
},
level: 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 &&
raids[index].sort((a, b) => a.element - b.element).map((item, i) => {
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 Link from 'next/link'
import { useCookies } from 'react-cookie'
import { useRouter } from 'next/router'
import { Trans, useTranslation } from 'next-i18next'
import { AxiosResponse } from 'axios'
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 SignupModal = (props: Props) => {
const router = useRouter()
const { t } = useTranslation('common')
// Set up form states and error handling
const [formValid, setFormValid] = useState(false)
const [errors, setErrors] = useState<ErrorMap>({
@ -56,7 +62,8 @@ const SignupModal = (props: Props) => {
username: usernameInput.current?.value,
email: emailInput.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,
}
// TODO: Set language
setCookies('user', cookieObj, { path: '/'})
accountState.account.language = user.language
accountState.account.user = {
id: user.id,
username: user.username,
@ -138,7 +145,7 @@ const SignupModal = (props: Props) => {
validateName(fieldName, value)
} else {
newErrors[fieldName] = `This ${fieldName} is already in use`
newErrors[fieldName] = t('modals.signup.errors.field_in_use', { field: fieldName})
setErrors(newErrors)
setFormValid(false)
}
@ -150,9 +157,9 @@ const SignupModal = (props: Props) => {
switch(fieldName) {
case 'username':
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)
newErrors.username = 'Username must be less than 20 characters'
newErrors.username = t('modals.signup.errors.username_too_long')
else
newErrors.username = ''
@ -161,7 +168,7 @@ const SignupModal = (props: Props) => {
case 'email':
newErrors.email = emailRegex.test(value)
? ''
: 'That email address is not valid'
: t('modals.signup.errors.invalid_email')
break
default:
@ -180,20 +187,20 @@ const SignupModal = (props: Props) => {
switch(name) {
case 'password':
newErrors.password = passwordInput.current?.value.includes(usernameInput.current?.value!)
? 'Your password should not contain your username'
? t('modals.signup.errors.password_contains_username')
: ''
break
case 'password':
newErrors.password = value.length < 8
? 'Password must be at least 8 characters'
? t('modals.signup.errors.password_too_short')
: ''
break
case 'confirm_password':
newErrors.passwordConfirmation = passwordInput.current?.value === passwordConfirmationInput.current?.value
? ''
: 'Your passwords don\'t match'
: t('modals.signup.errors.passwords_dont_match')
break
default:
@ -231,13 +238,13 @@ const SignupModal = (props: Props) => {
<Dialog.Root open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild>
<li className="MenuItem">
<span>Sign up</span>
<span>{t('menu.signup')}</span>
</li>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Content className="Signup Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }>
<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>
<span>
<CrossIcon />
@ -248,7 +255,7 @@ const SignupModal = (props: Props) => {
<form className="form" onSubmit={register}>
<Fieldset
fieldName="username"
placeholder="Username"
placeholder={t('modals.signup.placeholders.username')}
onChange={handleNameChange}
error={errors.username}
ref={usernameInput}
@ -256,7 +263,7 @@ const SignupModal = (props: Props) => {
<Fieldset
fieldName="email"
placeholder="Email address"
placeholder={t('modals.signup.placeholders.email')}
onChange={handleNameChange}
error={errors.email}
ref={emailInput}
@ -264,7 +271,7 @@ const SignupModal = (props: Props) => {
<Fieldset
fieldName="password"
placeholder="Password"
placeholder={t('modals.signup.placeholders.password')}
onChange={handlePasswordChange}
error={errors.password}
ref={passwordInput}
@ -272,16 +279,18 @@ const SignupModal = (props: Props) => {
<Fieldset
fieldName="confirm_password"
placeholder="Password (again)"
placeholder={t('modals.signup.placeholders.password_confirm')}
onChange={handlePasswordChange}
error={errors.passwordConfirmation}
ref={passwordConfirmationInput}
/>
<Button>Sign up</Button>
<Button>{t('modals.signup.buttons.confirm')}</Button>
<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>
</form>
</Dialog.Content>

View file

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

View file

@ -1,11 +1,12 @@
import React from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import * as HoverCard from '@radix-ui/react-hover-card'
import WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from '~components/UncapIndicator'
import { axData } from '~utils/axData'
import './index.scss'
interface Props {
@ -21,8 +22,11 @@ interface KeyNames {
}
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 Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana']
const tintElement = Element[props.gridSummon.object.element]
const wikiUrl = `https://gbf.wiki/${props.gridSummon.object.name.en.replaceAll(' ', '_')}`
@ -57,8 +61,8 @@ const SummonHovercard = (props: Props) => {
<HoverCard.Content className="Weapon Hovercard">
<div className="top">
<div className="title">
<h4>{ props.gridSummon.object.name.en }</h4>
<img alt={props.gridSummon.object.name.en} src={summonImage()} />
<h4>{ props.gridSummon.object.name[locale] }</h4>
<img alt={props.gridSummon.object.name[locale]} src={summonImage()} />
</div>
<div className="subInfo">
<div className="icons">
@ -72,7 +76,7 @@ const SummonHovercard = (props: Props) => {
/>
</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.Content>
</HoverCard.Root>

View file

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

View file

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

View file

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

View file

@ -1,4 +1,7 @@
import React from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import * as HoverCard from '@radix-ui/react-hover-card'
import WeaponLabelIcon from '~components/WeaponLabelIcon'
@ -15,30 +18,35 @@ interface Props {
interface KeyNames {
[key: string]: {
[key: string]: string
en: string,
jp: string
ja: string
}
}
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 Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana']
const WeaponKeyNames: KeyNames = {
'2': {
en: 'Pendulum',
jp: ''
ja: 'ペンデュラム'
},
'3': {
en: 'Teluma',
jp: ''
ja: 'テルマ'
},
'17': {
en: 'Gauph Key',
jp: ''
ja: 'ガフスキー'
},
'22': {
en: 'Emblem',
jp: ''
ja: 'エンブレム'
}
}
@ -61,7 +69,7 @@ const WeaponHovercard = (props: Props) => {
const simpleAxSkill = props.gridWeapon.ax[0]
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 ''
@ -78,7 +86,7 @@ const WeaponHovercard = (props: Props) => {
if (primaryAxSkill && primaryAxSkill.secondary) {
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 = (
<section className="weaponKeys">
{ (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) ?
Array.from(Array(props.gridWeapon.weapon_keys.length)).map((x, i) => {
return (
<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>
)
}) : '' }
@ -113,16 +121,16 @@ const WeaponHovercard = (props: Props) => {
const axSection = (
<section className="axSkills">
<h5 className={tintElement}>AX Skills</h5>
<h5 className={tintElement}>{t('modals.weapon.subtitles.ax_skills')}</h5>
<div className="skills">
<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>
</div>
{ (props.gridWeapon.ax && props.gridWeapon.ax[1].modifier && props.gridWeapon.ax[1].strength) ?
<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>
</div> : ''}
</div>
@ -137,8 +145,8 @@ const WeaponHovercard = (props: Props) => {
<HoverCard.Content className="Weapon Hovercard" side={hovercardSide()}>
<div className="top">
<div className="title">
<h4>{ props.gridWeapon.object.name.en }</h4>
<img alt={props.gridWeapon.object.name.en} src={weaponImage()} />
<h4>{ props.gridWeapon.object.name[locale] }</h4>
<img alt={props.gridWeapon.object.name[locale]} src={weaponImage()} />
</div>
<div className="subInfo">
<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.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.Content>
</HoverCard.Root>

View file

@ -6,74 +6,142 @@
/* Elements */
&.fire {
background-image: url('/labels/element/Label_Element_Fire.png')
&.fire.en {
background-image: url('/labels/element/fire_en.png')
}
&.water {
background-image: url('/labels/element/Label_Element_Water.png')
&.fire.ja {
background-image: url('/labels/element/fire_ja.png')
}
&.earth {
background-image: url('/labels/element/Label_Element_Earth.png')
&.water.en {
background-image: url('/labels/element/water_en.png')
}
&.wind {
background-image: url('/labels/element/Label_Element_Wind.png')
&.water.ja {
background-image: url('/labels/element/water_ja.png')
}
&.dark {
background-image: url('/labels/element/Label_Element_Dark.png')
&.earth.en {
background-image: url('/labels/element/earth_en.png')
}
&.light {
background-image: url('/labels/element/Label_Element_Light.png')
&.earth.ja {
background-image: url('/labels/element/earth_ja.png')
}
&.null {
background-image: url('/labels/element/Label_Element_Any.png')
&.wind.en {
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 */
&.sword {
background-image: url('/labels/proficiency/Label_Weapon_Sabre.png')
&.sword.en {
background-image: url('/labels/proficiency/sabre_en.png')
}
&.dagger {
background-image: url('/labels/proficiency/Label_Weapon_Dagger.png')
&.sword.ja {
background-image: url('/labels/proficiency/sabre_ja.png')
}
&.axe {
background-image: url('/labels/proficiency/Label_Weapon_Axe.png')
&.dagger.en {
background-image: url('/labels/proficiency/dagger_en.png')
}
&.spear {
background-image: url('/labels/proficiency/Label_Weapon_Spear.png')
&.dagger.ja {
background-image: url('/labels/proficiency/dagger_ja.png')
}
&.staff {
background-image: url('/labels/proficiency/Label_Weapon_Staff.png')
&.axe.en {
background-image: url('/labels/proficiency/axe_en.png')
}
&.fist {
background-image: url('/labels/proficiency/Label_Weapon_Melee.png')
&.axe.ja {
background-image: url('/labels/proficiency/axe_ja.png')
}
&.harp {
background-image: url('/labels/proficiency/Label_Weapon_Harp.png')
&.spear.en {
background-image: url('/labels/proficiency/spear_en.png')
}
&.gun {
background-image: url('/labels/proficiency/Label_Weapon_Gun.png')
&.spear.ja {
background-image: url('/labels/proficiency/spear_ja.png')
}
&.bow {
background-image: url('/labels/proficiency/Label_Weapon_Bow.png')
&.staff.en {
background-image: url('/labels/proficiency/staff_en.png')
}
&.katana {
background-image: url('/labels/proficiency/Label_Weapon_Katana.png')
&.staff.ja {
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 { useRouter } from 'next/router'
import './index.scss'
@ -6,12 +7,12 @@ interface Props {
labelType: string
}
class WeaponLabelIcon extends React.Component<Props> {
render() {
return (
<i className={`WeaponLabelIcon ${this.props.labelType}`} />
)
}
const WeaponLabelIcon = (props: Props) => {
const router = useRouter()
return (
<i className={`WeaponLabelIcon ${props.labelType} ${router.locale}`} />
)
}
export default WeaponLabelIcon

View file

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

View file

@ -1,4 +1,6 @@
import React from 'react'
import { useRouter } from 'next/router'
import UncapIndicator from '~components/UncapIndicator'
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 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> {
render() {
const weapon = this.props.data
const WeaponResult = (props: Props) => {
const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const weapon = props.data
return (
<li className="WeaponResult" onClick={this.props.onClick}>
<img alt={weapon.name.en} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`} />
<div className="Info">
<h5>{weapon.name.en}</h5>
<UncapIndicator
type="weapon"
flb={weapon.uncap.flb}
ulb={weapon.uncap.ulb}
special={false}
/>
<div className="tags">
<WeaponLabelIcon labelType={Element[weapon.element]} />
<WeaponLabelIcon labelType={Proficiency[weapon.proficiency]} />
</div>
return (
<li className="WeaponResult" onClick={props.onClick}>
<img alt={weapon.name[locale]} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`} />
<div className="Info">
<h5>{weapon.name[locale]}</h5>
<UncapIndicator
type="weapon"
flb={weapon.uncap.flb}
ulb={weapon.uncap.ulb}
special={false}
/>
<div className="tags">
<WeaponLabelIcon labelType={Element[weapon.element]} />
<WeaponLabelIcon labelType={Proficiency[weapon.proficiency]} />
</div>
</li>
)
}
</div>
</li>
)
}
export default WeaponResult

View file

@ -1,4 +1,6 @@
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import classnames from 'classnames'
import SearchModal from '~components/SearchModal'
@ -22,8 +24,13 @@ interface Props {
}
const WeaponUnit = (props: Props) => {
const { t } = useTranslation('common')
const [imageUrl, setImageUrl] = useState('')
const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const classes = classnames({
WeaponUnit: true,
'mainhand': props.unitType == 0,
@ -81,7 +88,7 @@ const WeaponUnit = (props: Props) => {
const editableImage = (
<SearchModal
placeholderText="Search for a weapon..."
placeholderText={t('search.placeholders.weapon')}
fromPosition={props.position}
object="weapons"
send={props.updateObject}>
@ -108,7 +115,7 @@ const WeaponUnit = (props: Props) => {
special={false}
/> : ''
}
<h3 className="WeaponName">{weapon?.name.en}</h3>
<h3 className="WeaponName">{weapon?.name[locale]}</h3>
</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} */
const path = require('path')
const { i18n } = require('./next-i18next.config')
module.exports = {
reactStrictMode: true,
@ -7,6 +8,7 @@ module.exports = {
prependData: '@import "variables";',
includePaths: [path.join(__dirname, 'styles')],
},
i18n,
async rewrites() {
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",
"axios": "^0.25.0",
"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.debounce": "^4.0.8",
"meyer-reset-scss": "^2.0.4",
"next": "12.0.8",
"next-i18next": "^10.5.0",
"next-remote-watch": "^1.0.0",
"react": "17.0.2",
"react-cookie": "^4.1.1",
"react-dom": "^17.0.2",
"react-i18next": "^11.15.3",
"react-i18next": "^11.15.5",
"react-linkify": "^1.0.0-alpha",
"react-scroll": "^1.8.5",
"sass": "^1.49.0",

View file

@ -1,7 +1,10 @@
import React, { useCallback, useEffect, useState } from 'react'
import Head from 'next/head'
import { useRouter } from 'next/router'
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'
@ -13,6 +16,7 @@ const ProfileRoute: React.FC = () => {
const router = useRouter()
const { username } = router.query
const { t } = useTranslation('common')
const [cookies] = useCookies(['account'])
const [found, setFound] = useState(false)
@ -169,7 +173,7 @@ const ProfileRoute: React.FC = () => {
</GridRepCollection>
{ (parties.length == 0) ?
<div id="NotFound">
<h2>{ (loading) ? 'Loading teams...' : 'No teams found' }</h2>
<h2>{ (loading) ? t('teams.loading') : t('teams.not_found') }</h2>
</div>
: '' }
</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 { useCookies, CookiesProvider } from 'react-cookie'
import { appWithTranslation } from 'next-i18next'
import type { AppProps } from 'next/app'
import Layout from '~components/Layout'
@ -16,7 +17,6 @@ function MyApp({ Component, pageProps }: AppProps) {
console.log(`Logged in as user "${cookies.account.username}"`)
accountState.account.authorized = true
accountState.account.language = cookies.account.language
accountState.account.user = {
id: cookies.account.user_id,
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 Party from '~components/Party'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
const NewRoute = () => {
function callback(path: string) {
// 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

View file

@ -1,5 +1,6 @@
import React from 'react'
import { useRouter } from 'next/router'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
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

View file

@ -1,8 +1,11 @@
import React, { useCallback, useEffect, useState } from 'react'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { useCookies } from 'react-cookie'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import clonedeep from 'lodash.clonedeep'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import api from '~utils/api'
@ -12,6 +15,7 @@ import FilterBar from '~components/FilterBar'
const SavedRoute: React.FC = () => {
const router = useRouter()
const { t } = useTranslation('common')
// Cookies
const [cookies] = useCookies(['account'])
@ -143,7 +147,7 @@ const SavedRoute: React.FC = () => {
return (
<div id="Teams">
<Head>
<title>Your saved Teams</title>
<title>{t('saved.title')}</title>
<meta property="og:title" content="Your saved Teams" />
<meta property="og:url" content="https://app.granblue.team/saved" />
@ -155,7 +159,7 @@ const SavedRoute: React.FC = () => {
</Head>
<FilterBar onFilter={receiveFilters} scrolled={scrolled}>
<h1>Your saved Teams</h1>
<h1>{t('saved.title')}</h1>
</FilterBar>
<section>
@ -182,7 +186,7 @@ const SavedRoute: React.FC = () => {
{ (parties.length == 0) ?
<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>
: '' }
</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

View file

@ -1,9 +1,15 @@
import React, { useCallback, useEffect, useState } from 'react'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { useCookies } from 'react-cookie'
import { useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import clonedeep from 'lodash.clonedeep'
import api from '~utils/api'
import GridRep from '~components/GridRep'
@ -12,6 +18,7 @@ import FilterBar from '~components/FilterBar'
const TeamsRoute: React.FC = () => {
const router = useRouter()
const { t } = useTranslation('common')
// Cookies
const [cookies] = useCookies(['account'])
@ -148,7 +155,7 @@ const TeamsRoute: React.FC = () => {
return (
<div id="Teams">
<Head>
<title>Discover Teams</title>
<title>{ t('teams.title') }</title>
<meta property="og:title" content="Discover Teams" />
<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" />
</Head>
<FilterBar onFilter={receiveFilters} scrolled={scrolled}>
<h1>Discover Teams</h1>
<h1>{t('teams.title')}</h1>
</FilterBar>
<section>
@ -188,7 +195,7 @@ const TeamsRoute: React.FC = () => {
{ (parties.length == 0) ?
<div id="NotFound">
<h2>{ (loading) ? 'Loading teams...' : 'No teams found' }</h2>
<h2>{ (loading) ? t('teams.loading') : t('teams.not_found') }</h2>
</div>
: '' }
</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

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 {
name: {
[key: string]: string
en: string,
jp: string
ja: string
},
id: number,
minValue: number,

View file

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

3
types/Raid.d.ts vendored
View file

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

3
types/Summon.d.ts vendored
View file

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

3
types/Weapon.d.ts vendored
View file

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

View file

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

View file

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

View file

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

View file

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