commit
a3ac29deb8
34 changed files with 1637 additions and 670 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -50,7 +50,7 @@ dist/
|
|||
public/images/weapon*
|
||||
public/images/summon*
|
||||
public/images/chara*
|
||||
public/images/jobs
|
||||
public/images/job*
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
|
|
|||
52
components/Alert/index.scss
Normal file
52
components/Alert/index.scss
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
.AlertWrapper {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 21;
|
||||
}
|
||||
|
||||
.Alert {
|
||||
background: white;
|
||||
border-radius: $unit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
min-width: $unit * 20;
|
||||
max-width: $unit * 40;
|
||||
padding: $unit * 4;
|
||||
|
||||
.description {
|
||||
font-size: $font-regular;
|
||||
line-height: 1.26;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.Button {
|
||||
font-size: $font-regular;
|
||||
padding: ($unit * 1.5) ($unit * 2);
|
||||
margin-top: $unit * 2;
|
||||
|
||||
&.btn-disabled {
|
||||
background: $grey-90;
|
||||
color: $grey-70;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:not(.btn-disabled) {
|
||||
background: $grey-90;
|
||||
color: $grey-40;
|
||||
|
||||
&:hover {
|
||||
background: $grey-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
components/Alert/index.tsx
Normal file
51
components/Alert/index.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import React from "react"
|
||||
import * as AlertDialog from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import "./index.scss"
|
||||
import Button from "~components/Button"
|
||||
import { ButtonType } from "~utils/enums"
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
open: boolean
|
||||
title?: string
|
||||
message: string
|
||||
primaryAction?: () => void
|
||||
primaryActionText?: string
|
||||
cancelAction: () => void
|
||||
cancelActionText: string
|
||||
}
|
||||
|
||||
const Alert = (props: Props) => {
|
||||
return (
|
||||
<AlertDialog.Root open={props.open}>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay className="Overlay" onClick={props.cancelAction} />
|
||||
<div className="AlertWrapper">
|
||||
<AlertDialog.Content className="Alert">
|
||||
{props.title ? <AlertDialog.Title>Error</AlertDialog.Title> : ""}
|
||||
<AlertDialog.Description className="description">
|
||||
{props.message}
|
||||
</AlertDialog.Description>
|
||||
<div className="buttons">
|
||||
<AlertDialog.Cancel asChild>
|
||||
<Button onClick={props.cancelAction}>
|
||||
{props.cancelActionText}
|
||||
</Button>
|
||||
</AlertDialog.Cancel>
|
||||
{props.primaryAction ? (
|
||||
<AlertDialog.Action onClick={props.primaryAction}>
|
||||
{props.primaryActionText}
|
||||
</AlertDialog.Action>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
</AlertDialog.Content>
|
||||
</div>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default Alert
|
||||
|
|
@ -40,7 +40,6 @@ const CharacterConflictModal = (props: Props) => {
|
|||
else if (uncap == 5) suffix = "03"
|
||||
else if (uncap > 2) suffix = "02"
|
||||
|
||||
console.log(appState.grid.weapons.mainWeapon)
|
||||
// Special casing for Lyria (and Young Cat eventually)
|
||||
if (character?.granblue_id === "3030182000") {
|
||||
let element = 1
|
||||
|
|
|
|||
|
|
@ -6,14 +6,17 @@ import { useSnapshot } from "valtio"
|
|||
import { AxiosResponse } from "axios"
|
||||
import debounce from "lodash.debounce"
|
||||
|
||||
import Alert from "~components/Alert"
|
||||
import JobSection from "~components/JobSection"
|
||||
import CharacterUnit from "~components/CharacterUnit"
|
||||
import CharacterConflictModal from "~components/CharacterConflictModal"
|
||||
|
||||
import type { JobSkillObject, SearchableObject } from "~types"
|
||||
|
||||
import api from "~utils/api"
|
||||
import { appState } from "~utils/appState"
|
||||
|
||||
import "./index.scss"
|
||||
import CharacterConflictModal from "~components/CharacterConflictModal"
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
|
|
@ -46,6 +49,16 @@ const CharacterGrid = (props: Props) => {
|
|||
const [conflicts, setConflicts] = useState<GridCharacter[]>([])
|
||||
const [position, setPosition] = useState(0)
|
||||
|
||||
// Set up state for data
|
||||
const [job, setJob] = useState<Job | undefined>()
|
||||
const [jobSkills, setJobSkills] = useState<JobSkillObject>({
|
||||
0: undefined,
|
||||
1: undefined,
|
||||
2: undefined,
|
||||
3: undefined,
|
||||
})
|
||||
const [errorMessage, setErrorMessage] = useState("")
|
||||
|
||||
// Create a temporary state to store previous character uncap values
|
||||
const [previousUncapValues, setPreviousUncapValues] = useState<{
|
||||
[key: number]: number | undefined
|
||||
|
|
@ -62,6 +75,11 @@ const CharacterGrid = (props: Props) => {
|
|||
else appState.party.editable = false
|
||||
}, [props.new, accountData, party])
|
||||
|
||||
useEffect(() => {
|
||||
setJob(appState.party.job)
|
||||
setJobSkills(appState.party.jobSkills)
|
||||
}, [appState])
|
||||
|
||||
// Initialize an array of current uncap values for each characters
|
||||
useEffect(() => {
|
||||
let initialPreviousUncapValues: { [key: number]: number } = {}
|
||||
|
|
@ -73,7 +91,7 @@ const CharacterGrid = (props: Props) => {
|
|||
|
||||
// Methods: Adding an object from search
|
||||
function receiveCharacterFromSearch(
|
||||
object: Character | Weapon | Summon,
|
||||
object: SearchableObject,
|
||||
position: number
|
||||
) {
|
||||
const character = object as Character
|
||||
|
|
@ -163,6 +181,69 @@ const CharacterGrid = (props: Props) => {
|
|||
setIncoming(undefined)
|
||||
}
|
||||
|
||||
// Methods: Saving job and job skills
|
||||
const saveJob = function (job: Job) {
|
||||
const payload = {
|
||||
party: {
|
||||
job_id: job ? job.id : "",
|
||||
},
|
||||
...headers,
|
||||
}
|
||||
|
||||
if (party.id && appState.party.editable) {
|
||||
api.updateJob({ partyId: party.id, params: payload }).then((response) => {
|
||||
const newParty = response.data.party
|
||||
|
||||
setJob(newParty.job)
|
||||
appState.party.job = newParty.job
|
||||
|
||||
setJobSkills(newParty.job_skills)
|
||||
appState.party.jobSkills = newParty.job_skills
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const saveJobSkill = function (skill: JobSkill, position: number) {
|
||||
if (party.id && appState.party.editable) {
|
||||
const positionedKey = `skill${position}_id`
|
||||
|
||||
let skillObject: {
|
||||
[key: string]: string | undefined
|
||||
skill0_id?: string
|
||||
skill1_id?: string
|
||||
skill2_id?: string
|
||||
skill3_id?: string
|
||||
} = {}
|
||||
|
||||
const payload = {
|
||||
party: skillObject,
|
||||
...headers,
|
||||
}
|
||||
|
||||
skillObject[positionedKey] = skill.id
|
||||
api
|
||||
.updateJobSkills({ partyId: party.id, params: payload })
|
||||
.then((response) => {
|
||||
// Update the current skills
|
||||
const newSkills = response.data.party.job_skills
|
||||
setJobSkills(newSkills)
|
||||
appState.party.jobSkills = newSkills
|
||||
})
|
||||
.catch((error) => {
|
||||
const data = error.response.data
|
||||
if (data.code == "too_many_skills_of_type") {
|
||||
const message = `You can only add up to 2 ${
|
||||
data.skill_type === "emp"
|
||||
? data.skill_type.toUpperCase()
|
||||
: data.skill_type
|
||||
} skills to your party at once.`
|
||||
setErrorMessage(message)
|
||||
}
|
||||
console.log(error.response.data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Methods: Helpers
|
||||
function characterUncapLevel(character: Character) {
|
||||
let uncapLevel
|
||||
|
|
@ -250,11 +331,27 @@ const CharacterGrid = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
function cancelAlert() {
|
||||
setErrorMessage("")
|
||||
}
|
||||
|
||||
// Render: JSX components
|
||||
return (
|
||||
<div>
|
||||
<Alert
|
||||
open={errorMessage.length > 0}
|
||||
message={errorMessage}
|
||||
cancelAction={cancelAlert}
|
||||
cancelActionText={"Got it"}
|
||||
/>
|
||||
<div id="CharacterGrid">
|
||||
<JobSection />
|
||||
<JobSection
|
||||
job={job}
|
||||
jobSkills={jobSkills}
|
||||
editable={party.editable}
|
||||
saveJob={saveJob}
|
||||
saveSkill={saveJobSkill}
|
||||
/>
|
||||
<CharacterConflictModal
|
||||
open={modalOpen}
|
||||
incomingCharacter={incoming}
|
||||
|
|
|
|||
|
|
@ -1,130 +1,137 @@
|
|||
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 React, { useEffect, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import classnames from "classnames"
|
||||
|
||||
import { appState } from '~utils/appState'
|
||||
import { appState } from "~utils/appState"
|
||||
|
||||
import CharacterHovercard from '~components/CharacterHovercard'
|
||||
import SearchModal from '~components/SearchModal'
|
||||
import UncapIndicator from '~components/UncapIndicator'
|
||||
import PlusIcon from '~public/icons/Add.svg'
|
||||
import CharacterHovercard from "~components/CharacterHovercard"
|
||||
import SearchModal from "~components/SearchModal"
|
||||
import UncapIndicator from "~components/UncapIndicator"
|
||||
import PlusIcon from "~public/icons/Add.svg"
|
||||
|
||||
import './index.scss'
|
||||
import { getRedirectStatus } from 'next/dist/lib/load-custom-routes'
|
||||
import type { SearchableObject } from "~types"
|
||||
|
||||
import "./index.scss"
|
||||
|
||||
interface Props {
|
||||
gridCharacter: GridCharacter | undefined
|
||||
position: number
|
||||
editable: boolean
|
||||
updateObject: (object: Character | Weapon | Summon, position: number) => void
|
||||
updateUncap: (id: string, position: number, uncap: number) => void
|
||||
gridCharacter?: GridCharacter
|
||||
position: number
|
||||
editable: boolean
|
||||
updateObject: (object: SearchableObject, position: number) => void
|
||||
updateUncap: (id: string, position: number, uncap: number) => void
|
||||
}
|
||||
|
||||
const CharacterUnit = (props: Props) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
const { party, grid } = useSnapshot(appState)
|
||||
const { party, grid } = useSnapshot(appState)
|
||||
|
||||
const router = useRouter()
|
||||
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
|
||||
|
||||
const [imageUrl, setImageUrl] = useState('')
|
||||
const [imageUrl, setImageUrl] = useState("")
|
||||
|
||||
const classes = classnames({
|
||||
CharacterUnit: true,
|
||||
'editable': props.editable,
|
||||
'filled': (props.gridCharacter !== undefined)
|
||||
})
|
||||
const classes = classnames({
|
||||
CharacterUnit: true,
|
||||
editable: props.editable,
|
||||
filled: props.gridCharacter !== undefined,
|
||||
})
|
||||
|
||||
const gridCharacter = props.gridCharacter
|
||||
const character = gridCharacter?.object
|
||||
const gridCharacter = props.gridCharacter
|
||||
const character = gridCharacter?.object
|
||||
|
||||
useEffect(() => {
|
||||
generateImageUrl()
|
||||
})
|
||||
useEffect(() => {
|
||||
generateImageUrl()
|
||||
})
|
||||
|
||||
function generateImageUrl() {
|
||||
let imgSrc = ""
|
||||
|
||||
if (props.gridCharacter) {
|
||||
const character = props.gridCharacter.object!
|
||||
function generateImageUrl() {
|
||||
let imgSrc = ""
|
||||
|
||||
// Change the image based on the uncap level
|
||||
let suffix = '01'
|
||||
if (props.gridCharacter.uncap_level == 6)
|
||||
suffix = '04'
|
||||
else if (props.gridCharacter.uncap_level == 5)
|
||||
suffix = '03'
|
||||
else if (props.gridCharacter.uncap_level > 2)
|
||||
suffix = '02'
|
||||
if (props.gridCharacter) {
|
||||
const character = props.gridCharacter.object!
|
||||
|
||||
// Special casing for Lyria (and Young Cat eventually)
|
||||
if (props.gridCharacter.object.granblue_id === '3030182000') {
|
||||
let element = 1
|
||||
if (grid.weapons.mainWeapon && grid.weapons.mainWeapon.element) {
|
||||
element = grid.weapons.mainWeapon.element
|
||||
} else if (party.element != 0) {
|
||||
element = party.element
|
||||
}
|
||||
// Change the image based on the uncap level
|
||||
let suffix = "01"
|
||||
if (props.gridCharacter.uncap_level == 6) suffix = "04"
|
||||
else if (props.gridCharacter.uncap_level == 5) suffix = "03"
|
||||
else if (props.gridCharacter.uncap_level > 2) suffix = "02"
|
||||
|
||||
suffix = `${suffix}_0${element}`
|
||||
}
|
||||
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-main/${character.granblue_id}_${suffix}.jpg`
|
||||
// Special casing for Lyria (and Young Cat eventually)
|
||||
if (props.gridCharacter.object.granblue_id === "3030182000") {
|
||||
let element = 1
|
||||
if (grid.weapons.mainWeapon && grid.weapons.mainWeapon.element) {
|
||||
element = grid.weapons.mainWeapon.element
|
||||
} else if (party.element != 0) {
|
||||
element = party.element
|
||||
}
|
||||
|
||||
setImageUrl(imgSrc)
|
||||
suffix = `${suffix}_0${element}`
|
||||
}
|
||||
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-main/${character.granblue_id}_${suffix}.jpg`
|
||||
}
|
||||
|
||||
function passUncapData(uncap: number) {
|
||||
if (props.gridCharacter)
|
||||
props.updateUncap(props.gridCharacter.id, props.position, uncap)
|
||||
}
|
||||
setImageUrl(imgSrc)
|
||||
}
|
||||
|
||||
const image = (
|
||||
<div className="CharacterImage">
|
||||
<img alt={character?.name.en} className="grid_image" src={imageUrl} />
|
||||
{ (props.editable) ? <span className='icon'><PlusIcon /></span> : '' }
|
||||
</div>
|
||||
)
|
||||
function passUncapData(uncap: number) {
|
||||
if (props.gridCharacter)
|
||||
props.updateUncap(props.gridCharacter.id, props.position, uncap)
|
||||
}
|
||||
|
||||
const editableImage = (
|
||||
<SearchModal
|
||||
placeholderText={t('search.placeholders.character')}
|
||||
fromPosition={props.position}
|
||||
object="characters"
|
||||
send={props.updateObject}>
|
||||
{image}
|
||||
</SearchModal>
|
||||
)
|
||||
const image = (
|
||||
<div className="CharacterImage">
|
||||
<img alt={character?.name.en} className="grid_image" src={imageUrl} />
|
||||
{props.editable ? (
|
||||
<span className="icon">
|
||||
<PlusIcon />
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const unitContent = (
|
||||
<div className={classes}>
|
||||
{ (props.editable) ? editableImage : image }
|
||||
{ (gridCharacter && character) ?
|
||||
<UncapIndicator
|
||||
type="character"
|
||||
flb={character.uncap.flb || false}
|
||||
ulb={character.uncap.ulb || false}
|
||||
uncapLevel={gridCharacter.uncap_level}
|
||||
updateUncap={passUncapData}
|
||||
special={character.special}
|
||||
/> : '' }
|
||||
<h3 className="CharacterName">{character?.name[locale]}</h3>
|
||||
</div>
|
||||
)
|
||||
const editableImage = (
|
||||
<SearchModal
|
||||
placeholderText={t("search.placeholders.character")}
|
||||
fromPosition={props.position}
|
||||
object="characters"
|
||||
send={props.updateObject}
|
||||
>
|
||||
{image}
|
||||
</SearchModal>
|
||||
)
|
||||
|
||||
const withHovercard = (
|
||||
<CharacterHovercard gridCharacter={gridCharacter!}>
|
||||
{unitContent}
|
||||
</CharacterHovercard>
|
||||
)
|
||||
const unitContent = (
|
||||
<div className={classes}>
|
||||
{props.editable ? editableImage : image}
|
||||
{gridCharacter && character ? (
|
||||
<UncapIndicator
|
||||
type="character"
|
||||
flb={character.uncap.flb || false}
|
||||
ulb={character.uncap.ulb || false}
|
||||
uncapLevel={gridCharacter.uncap_level}
|
||||
updateUncap={passUncapData}
|
||||
special={character.special}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<h3 className="CharacterName">{character?.name[locale]}</h3>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
(gridCharacter && !props.editable) ? withHovercard : unitContent
|
||||
)
|
||||
const withHovercard = (
|
||||
<CharacterHovercard gridCharacter={gridCharacter!}>
|
||||
{unitContent}
|
||||
</CharacterHovercard>
|
||||
)
|
||||
|
||||
return gridCharacter && !props.editable ? withHovercard : unitContent
|
||||
}
|
||||
|
||||
export default CharacterUnit
|
||||
|
|
|
|||
|
|
@ -1,47 +1,46 @@
|
|||
import React from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import SummonUnit from '~components/SummonUnit'
|
||||
import './index.scss'
|
||||
import React from "react"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import SummonUnit from "~components/SummonUnit"
|
||||
import { SearchableObject } from "~types"
|
||||
import "./index.scss"
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
grid: GridArray<GridSummon>
|
||||
editable: boolean
|
||||
exists: boolean
|
||||
found?: boolean
|
||||
offset: number
|
||||
updateObject: (object: Character | Weapon | Summon, position: number) => void
|
||||
updateUncap: (id: string, position: number, uncap: number) => void
|
||||
grid: GridArray<GridSummon>
|
||||
editable: boolean
|
||||
exists: boolean
|
||||
found?: boolean
|
||||
offset: number
|
||||
updateObject: (object: SearchableObject, position: number) => void
|
||||
updateUncap: (id: string, position: number, uncap: number) => void
|
||||
}
|
||||
|
||||
const ExtraSummons = (props: Props) => {
|
||||
const numSummons: number = 2
|
||||
const numSummons: number = 2
|
||||
|
||||
const { t } = useTranslation('common')
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
return (
|
||||
<div id="ExtraSummons">
|
||||
<span>{t('summons.subaura')}</span>
|
||||
<ul id="grid_summons">
|
||||
{
|
||||
Array.from(Array(numSummons)).map((x, i) => {
|
||||
return (
|
||||
<li key={`grid_unit_${i}`} >
|
||||
<SummonUnit
|
||||
editable={props.editable}
|
||||
position={props.offset + i}
|
||||
unitType={1}
|
||||
gridSummon={props.grid[props.offset + i]}
|
||||
updateObject={props.updateObject}
|
||||
updateUncap={props.updateUncap}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div id="ExtraSummons">
|
||||
<span>{t("summons.subaura")}</span>
|
||||
<ul id="grid_summons">
|
||||
{Array.from(Array(numSummons)).map((x, i) => {
|
||||
return (
|
||||
<li key={`grid_unit_${i}`}>
|
||||
<SummonUnit
|
||||
editable={props.editable}
|
||||
position={props.offset + i}
|
||||
unitType={1}
|
||||
gridSummon={props.grid[props.offset + i]}
|
||||
updateObject={props.updateObject}
|
||||
updateUncap={props.updateUncap}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExtraSummons
|
||||
|
|
|
|||
|
|
@ -1,97 +1,113 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { useSnapshot } from "valtio"
|
||||
|
||||
import api from '~utils/api'
|
||||
import { appState } from '~utils/appState'
|
||||
import { jobGroups } from '~utils/jobGroups'
|
||||
import { appState } from "~utils/appState"
|
||||
import { jobGroups } from "~utils/jobGroups"
|
||||
|
||||
import './index.scss'
|
||||
import "./index.scss"
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
currentJob?: string
|
||||
onChange?: (job?: Job) => void
|
||||
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
||||
currentJob?: string
|
||||
onChange?: (job?: Job) => void
|
||||
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
||||
}
|
||||
|
||||
type GroupedJob = { [key: string]: Job[] }
|
||||
|
||||
const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFieldSet(props, ref) {
|
||||
const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
|
||||
function useFieldSet(props, ref) {
|
||||
// Set up router for locale
|
||||
const router = useRouter()
|
||||
const locale = router.locale || 'en'
|
||||
const locale = router.locale || "en"
|
||||
|
||||
// Create snapshot of app state
|
||||
const { party } = useSnapshot(appState)
|
||||
|
||||
// Set up local states for storing jobs
|
||||
const [currentJob, setCurrentJob] = useState<Job>()
|
||||
const [jobs, setJobs] = useState<Job[]>()
|
||||
const [sortedJobs, setSortedJobs] = useState<GroupedJob>()
|
||||
|
||||
// Organize jobs into groups on mount
|
||||
const organizeJobs = useCallback((jobs: Job[]) => {
|
||||
const jobGroups = jobs.map(job => job.row).filter((value, index, self) => self.indexOf(value) === index)
|
||||
let groupedJobs: GroupedJob = {}
|
||||
|
||||
jobGroups.forEach(group => {
|
||||
groupedJobs[group] = jobs.filter(job => job.row === group)
|
||||
})
|
||||
|
||||
setJobs(jobs)
|
||||
setSortedJobs(groupedJobs)
|
||||
appState.jobs = jobs
|
||||
// Set current job from state on mount
|
||||
useEffect(() => {
|
||||
setCurrentJob(party.job)
|
||||
}, [])
|
||||
|
||||
// Fetch all jobs on mount
|
||||
// Organize jobs into groups on mount
|
||||
useEffect(() => {
|
||||
api.endpoints.jobs.getAll()
|
||||
.then(response => organizeJobs(response.data))
|
||||
}, [organizeJobs])
|
||||
const jobGroups = appState.jobs
|
||||
.map((job) => job.row)
|
||||
.filter((value, index, self) => self.indexOf(value) === index)
|
||||
let groupedJobs: GroupedJob = {}
|
||||
|
||||
jobGroups.forEach((group) => {
|
||||
groupedJobs[group] = appState.jobs.filter((job) => job.row === group)
|
||||
})
|
||||
|
||||
setJobs(appState.jobs)
|
||||
setSortedJobs(groupedJobs)
|
||||
}, [appState])
|
||||
|
||||
// Set current job on mount
|
||||
useEffect(() => {
|
||||
if (jobs && props.currentJob) {
|
||||
const job = jobs.find(job => job.id === props.currentJob)
|
||||
setCurrentJob(job)
|
||||
}
|
||||
}, [jobs, props.currentJob])
|
||||
if (jobs && props.currentJob) {
|
||||
const job = appState.jobs.find((job) => job.id === props.currentJob)
|
||||
setCurrentJob(job)
|
||||
}
|
||||
}, [appState, props.currentJob])
|
||||
|
||||
// Enable changing select value
|
||||
function handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
if (jobs) {
|
||||
const job = jobs.find(job => job.id === event.target.value)
|
||||
if (props.onChange) props.onChange(job)
|
||||
setCurrentJob(job)
|
||||
}
|
||||
if (jobs) {
|
||||
const job = jobs.find((job) => job.id === event.target.value)
|
||||
if (props.onChange) props.onChange(job)
|
||||
setCurrentJob(job)
|
||||
}
|
||||
}
|
||||
|
||||
// Render JSX for each job option, sorted into optgroups
|
||||
function renderJobGroup(group: string) {
|
||||
const options = sortedJobs && sortedJobs[group].length > 0 &&
|
||||
sortedJobs[group].sort((a, b) => a.order - b.order).map((item, i) => {
|
||||
return (
|
||||
<option key={i} value={item.id}>{item.name[locale]}</option>
|
||||
)
|
||||
})
|
||||
const options =
|
||||
sortedJobs &&
|
||||
sortedJobs[group].length > 0 &&
|
||||
sortedJobs[group]
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map((item, i) => {
|
||||
return (
|
||||
<option key={i} value={item.id}>
|
||||
{item.name[locale]}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
|
||||
const groupName = jobGroups.find(g => g.slug === group)?.name[locale]
|
||||
const groupName = jobGroups.find((g) => g.slug === group)?.name[locale]
|
||||
|
||||
return (
|
||||
<optgroup key={group} label={groupName}>
|
||||
{options}
|
||||
</optgroup>
|
||||
)
|
||||
return (
|
||||
<optgroup key={group} label={groupName}>
|
||||
{options}
|
||||
</optgroup>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<select
|
||||
key={currentJob?.id}
|
||||
value={currentJob?.id}
|
||||
onBlur={props.onBlur}
|
||||
onChange={handleChange}
|
||||
ref={ref}>
|
||||
<option key="no-job" value={-1}>No class</option>
|
||||
{ (sortedJobs) ? Object.keys(sortedJobs).map(x => renderJobGroup(x)) : '' }
|
||||
</select>
|
||||
<select
|
||||
key={currentJob ? currentJob.id : -1}
|
||||
value={currentJob ? currentJob.id : -1}
|
||||
onBlur={props.onBlur}
|
||||
onChange={handleChange}
|
||||
ref={ref}
|
||||
>
|
||||
<option key="no-job" value={-1}>
|
||||
No class
|
||||
</option>
|
||||
{sortedJobs
|
||||
? Object.keys(sortedJobs).map((x) => renderJobGroup(x))
|
||||
: ""}
|
||||
</select>
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default JobDropdown
|
||||
|
|
|
|||
|
|
@ -1,44 +1,73 @@
|
|||
#Job {
|
||||
display: flex;
|
||||
margin-bottom: $unit * 3;
|
||||
|
||||
select {
|
||||
flex-grow: 1;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.JobDetails {
|
||||
display: flex;
|
||||
margin-bottom: $unit * 3;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
h3 {
|
||||
font-size: $font-medium;
|
||||
font-weight: $medium;
|
||||
padding: $unit 0 $unit * 2;
|
||||
}
|
||||
|
||||
select {
|
||||
flex-grow: 1;
|
||||
width: auto;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.JobImage {
|
||||
$height: 249px;
|
||||
$width: 447px;
|
||||
|
||||
background: url('/images/background_a.jpg');
|
||||
background-size: 500px 281px;
|
||||
border-radius: $unit;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
display: block;
|
||||
flex-grow: 2;
|
||||
height: $height;
|
||||
margin-right: $unit * 3;
|
||||
max-height: $height;
|
||||
max-width: $width;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: $width;
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
top: $unit * -4;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.Overlay {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
.JobSkills {
|
||||
flex-grow: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.JobImage {
|
||||
$height: 249px;
|
||||
$width: 447px;
|
||||
|
||||
background: url("/images/background_a.jpg");
|
||||
background-size: 500px 281px;
|
||||
border-radius: $unit;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
display: block;
|
||||
flex-grow: 2;
|
||||
flex-shrink: 0;
|
||||
height: $height;
|
||||
margin-right: $unit * 3;
|
||||
max-height: $height;
|
||||
max-width: $width;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: $width;
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
|
||||
img {
|
||||
-webkit-filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.48));
|
||||
filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.48));
|
||||
position: relative;
|
||||
top: $unit * -4;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.Overlay {
|
||||
background: none;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.JobSkills {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +1,165 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import React, { ForwardedRef, useEffect, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { useTranslation } from "next-i18next"
|
||||
|
||||
import JobDropdown from '~components/JobDropdown'
|
||||
import JobDropdown from "~components/JobDropdown"
|
||||
import JobSkillItem from "~components/JobSkillItem"
|
||||
import SearchModal from "~components/SearchModal"
|
||||
|
||||
import { appState } from '~utils/appState'
|
||||
import { appState } from "~utils/appState"
|
||||
|
||||
import './index.scss'
|
||||
import type { JobSkillObject, SearchableObject } from "~types"
|
||||
|
||||
import "./index.scss"
|
||||
|
||||
// Props
|
||||
interface Props {}
|
||||
|
||||
const JobSection = (props: Props) => {
|
||||
const [job, setJob] = useState<Job>()
|
||||
const [imageUrl, setImageUrl] = useState('')
|
||||
|
||||
const { party } = useSnapshot(appState)
|
||||
|
||||
useEffect(() => {
|
||||
// Set current job based on ID
|
||||
setJob(party.job)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
generateImageUrl()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (job) appState.party.job = job
|
||||
}, [job])
|
||||
|
||||
function receiveJob(job?: Job) {
|
||||
setJob(job)
|
||||
}
|
||||
|
||||
function generateImageUrl() {
|
||||
let imgSrc = ""
|
||||
|
||||
if (job) {
|
||||
const slug = job?.name.en.replaceAll(' ', '-').toLowerCase()
|
||||
const gender = (party.user && party.user.gender == 1) ? 'b' : 'a'
|
||||
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/jobs/${slug}_${gender}.png`
|
||||
}
|
||||
|
||||
setImageUrl(imgSrc)
|
||||
}
|
||||
|
||||
// Render: JSX components
|
||||
return (
|
||||
<section id="Job">
|
||||
<div className="JobImage">
|
||||
<img src={imageUrl} />
|
||||
<div className="Overlay" />
|
||||
</div>
|
||||
<JobDropdown
|
||||
currentJob={ (party.job) ? party.job.id : undefined}
|
||||
onChange={receiveJob}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
interface Props {
|
||||
job?: Job
|
||||
jobSkills: JobSkillObject
|
||||
editable: boolean
|
||||
saveJob: (job: Job) => void
|
||||
saveSkill: (skill: JobSkill, position: number) => void
|
||||
}
|
||||
|
||||
export default JobSection
|
||||
const JobSection = (props: Props) => {
|
||||
const { party } = useSnapshot(appState)
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
|
||||
|
||||
const [job, setJob] = useState<Job>()
|
||||
const [imageUrl, setImageUrl] = useState("")
|
||||
const [numSkills, setNumSkills] = useState(4)
|
||||
const [skills, setSkills] = useState<{ [key: number]: JobSkill | undefined }>(
|
||||
[]
|
||||
)
|
||||
|
||||
const selectRef = React.createRef<HTMLSelectElement>()
|
||||
|
||||
useEffect(() => {
|
||||
// Set current job based on ID
|
||||
if (props.job) {
|
||||
setJob(props.job)
|
||||
setSkills({
|
||||
0: props.jobSkills[0],
|
||||
1: props.jobSkills[1],
|
||||
2: props.jobSkills[2],
|
||||
3: props.jobSkills[3],
|
||||
})
|
||||
|
||||
if (selectRef.current) selectRef.current.value = props.job.id
|
||||
}
|
||||
}, [props])
|
||||
|
||||
useEffect(() => {
|
||||
generateImageUrl()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (job) {
|
||||
if ((party.job && job.id != party.job.id) || !party.job)
|
||||
appState.party.job = job
|
||||
if (job.row === "1") setNumSkills(3)
|
||||
else setNumSkills(4)
|
||||
}
|
||||
}, [job])
|
||||
|
||||
function receiveJob(job?: Job) {
|
||||
if (job) {
|
||||
setJob(job)
|
||||
props.saveJob(job)
|
||||
}
|
||||
}
|
||||
|
||||
function generateImageUrl() {
|
||||
let imgSrc = ""
|
||||
|
||||
if (job) {
|
||||
const slug = job?.name.en.replaceAll(" ", "-").toLowerCase()
|
||||
const gender = party.user && party.user.gender == 1 ? "b" : "a"
|
||||
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/jobs/${slug}_${gender}.png`
|
||||
}
|
||||
|
||||
setImageUrl(imgSrc)
|
||||
}
|
||||
|
||||
const canEditSkill = (skill?: JobSkill) => {
|
||||
if (job && skill) {
|
||||
if (skill.job.id === job.id && skill.main && !skill.sub) return false
|
||||
}
|
||||
|
||||
return props.editable
|
||||
}
|
||||
|
||||
const skillItem = (index: number, editable: boolean) => {
|
||||
return (
|
||||
<JobSkillItem
|
||||
skill={skills[index]}
|
||||
editable={canEditSkill(skills[index])}
|
||||
key={`skill-${index}`}
|
||||
hasJob={job != undefined && job.id != "-1"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const editableSkillItem = (index: number) => {
|
||||
return (
|
||||
<SearchModal
|
||||
placeholderText={t("search.placeholders.job_skill")}
|
||||
fromPosition={index}
|
||||
object="job_skills"
|
||||
job={job}
|
||||
send={saveJobSkill}
|
||||
>
|
||||
{skillItem(index, true)}
|
||||
</SearchModal>
|
||||
)
|
||||
}
|
||||
|
||||
function saveJobSkill(object: SearchableObject, position: number) {
|
||||
const skill = object as JobSkill
|
||||
|
||||
const newSkills = skills
|
||||
newSkills[position] = skill
|
||||
setSkills(newSkills)
|
||||
|
||||
props.saveSkill(skill, position)
|
||||
}
|
||||
|
||||
// Render: JSX components
|
||||
return (
|
||||
<section id="Job">
|
||||
<div className="JobImage">
|
||||
<img src={imageUrl} />
|
||||
<div className="Overlay" />
|
||||
</div>
|
||||
<div className="JobDetails">
|
||||
{props.editable ? (
|
||||
<JobDropdown
|
||||
currentJob={party.job?.id}
|
||||
onChange={receiveJob}
|
||||
ref={selectRef}
|
||||
/>
|
||||
) : (
|
||||
<h3>{party.job?.name[locale]}</h3>
|
||||
)}
|
||||
|
||||
<ul className="JobSkills">
|
||||
{[...Array(numSkills)].map((e, i) => (
|
||||
<li key={`job-${i}`}>
|
||||
{canEditSkill(skills[i])
|
||||
? editableSkillItem(i)
|
||||
: skillItem(i, false)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default JobSection
|
||||
|
|
|
|||
46
components/JobSkillItem/index.scss
Normal file
46
components/JobSkillItem/index.scss
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
.JobSkill {
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
align-items: center;
|
||||
|
||||
&.editable:hover {
|
||||
cursor: pointer;
|
||||
|
||||
& > img.editable,
|
||||
& > div.placeholder.editable {
|
||||
border: $hover-stroke;
|
||||
box-shadow: $hover-shadow;
|
||||
cursor: pointer;
|
||||
transform: $scale-tall;
|
||||
}
|
||||
|
||||
& p.placeholder {
|
||||
color: $grey-20;
|
||||
}
|
||||
}
|
||||
|
||||
& > img,
|
||||
& > div.placeholder {
|
||||
background: white;
|
||||
border-radius: calc($unit / 2);
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
width: $unit * 5;
|
||||
height: $unit * 5;
|
||||
}
|
||||
|
||||
& > div.placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& > svg {
|
||||
fill: $grey-60;
|
||||
width: $unit * 2;
|
||||
height: $unit * 2;
|
||||
}
|
||||
}
|
||||
|
||||
p.placeholder {
|
||||
color: $grey-50;
|
||||
}
|
||||
}
|
||||
81
components/JobSkillItem/index.tsx
Normal file
81
components/JobSkillItem/index.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import React from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { useTranslation } from "next-i18next"
|
||||
|
||||
import classNames from "classnames"
|
||||
import PlusIcon from "~public/icons/Add.svg"
|
||||
|
||||
import "./index.scss"
|
||||
|
||||
// Props
|
||||
interface Props extends React.ComponentPropsWithoutRef<"div"> {
|
||||
skill?: JobSkill
|
||||
editable: boolean
|
||||
hasJob: boolean
|
||||
}
|
||||
|
||||
const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
|
||||
({ ...props }, forwardedRef) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation("common")
|
||||
const locale =
|
||||
router.locale && ["en", "ja"].includes(router.locale)
|
||||
? router.locale
|
||||
: "en"
|
||||
|
||||
const classes = classNames({
|
||||
JobSkill: true,
|
||||
editable: props.editable,
|
||||
})
|
||||
|
||||
const imageClasses = classNames({
|
||||
placeholder: !props.skill,
|
||||
editable: props.editable && props.hasJob,
|
||||
})
|
||||
|
||||
const skillImage = () => {
|
||||
let jsx: React.ReactNode
|
||||
|
||||
if (props.skill) {
|
||||
jsx = (
|
||||
<img
|
||||
alt={props.skill.name[locale]}
|
||||
className={imageClasses}
|
||||
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}job-skills/${props.skill.slug}.png`}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
jsx = (
|
||||
<div className={imageClasses}>
|
||||
{props.editable && props.hasJob ? <PlusIcon /> : ""}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return jsx
|
||||
}
|
||||
|
||||
const label = () => {
|
||||
let jsx: React.ReactNode
|
||||
|
||||
if (props.skill) {
|
||||
jsx = <p>{props.skill.name[locale]}</p>
|
||||
} else if (props.editable && props.hasJob) {
|
||||
jsx = <p className="placeholder">{t("job_skills.state.selectable")}</p>
|
||||
} else {
|
||||
jsx = <p className="placeholder">{t("job_skills.state.no_skill")}</p>
|
||||
}
|
||||
|
||||
return jsx
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes} onClick={props.onClick} ref={forwardedRef}>
|
||||
{skillImage()}
|
||||
{label()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default JobSkillItem
|
||||
71
components/JobSkillResult/index.scss
Normal file
71
components/JobSkillResult/index.scss
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
.JobSkillResult {
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
padding: $unit * 1.5;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background: $grey-90;
|
||||
cursor: pointer;
|
||||
|
||||
.Info .skill.pill {
|
||||
background: $grey-80;
|
||||
}
|
||||
}
|
||||
|
||||
.Info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: calc($unit / 2);
|
||||
width: 100%;
|
||||
|
||||
.skill.pill {
|
||||
background: $grey-90;
|
||||
border-radius: $unit * 2;
|
||||
color: $grey-00;
|
||||
display: inline;
|
||||
font-size: $font-tiny;
|
||||
font-weight: $medium;
|
||||
padding: calc($unit / 2) $unit;
|
||||
|
||||
&.buffing {
|
||||
background-color: $light-bg-dark;
|
||||
color: $light-text-dark;
|
||||
}
|
||||
|
||||
&.debuffing {
|
||||
background-color: $water-bg-dark;
|
||||
color: $water-text-dark;
|
||||
}
|
||||
|
||||
&.healing {
|
||||
background-color: $wind-bg-dark;
|
||||
color: $wind-text-dark;
|
||||
}
|
||||
|
||||
&.damaging {
|
||||
background-color: $fire-bg-dark;
|
||||
color: $fire-text-dark;
|
||||
}
|
||||
|
||||
&.field {
|
||||
background-color: $dark-bg-dark;
|
||||
color: $dark-text-dark;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
color: #555;
|
||||
display: inline-block;
|
||||
font-size: $font-medium;
|
||||
font-weight: $medium;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: $unit * 6;
|
||||
height: $unit * 6;
|
||||
}
|
||||
}
|
||||
41
components/JobSkillResult/index.tsx
Normal file
41
components/JobSkillResult/index.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import React, { useEffect, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { SkillGroup, skillClassification } from "~utils/skillGroups"
|
||||
|
||||
import "./index.scss"
|
||||
|
||||
interface Props {
|
||||
data: JobSkill
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const JobSkillResult = (props: Props) => {
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
|
||||
|
||||
const skill = props.data
|
||||
|
||||
const [group, setGroup] = useState<SkillGroup | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
setGroup(skillClassification.find((group) => group.id === skill.color))
|
||||
}, [skill, setGroup, skillClassification])
|
||||
|
||||
const jobSkillUrl = () =>
|
||||
`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-skills/${skill.slug}.png`
|
||||
|
||||
return (
|
||||
<li className="JobSkillResult" onClick={props.onClick}>
|
||||
<img alt={skill.name[locale]} src={jobSkillUrl()} />
|
||||
<div className="Info">
|
||||
<h5>{skill.name[locale]}</h5>
|
||||
<div className={`skill pill ${group?.name["en"].toLowerCase()}`}>
|
||||
{group?.name[locale]}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default JobSkillResult
|
||||
3
components/JobSkillSearchFilterBar/index.scss
Normal file
3
components/JobSkillSearchFilterBar/index.scss
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.SearchFilterBar select {
|
||||
background-color: $grey-90;
|
||||
}
|
||||
71
components/JobSkillSearchFilterBar/index.tsx
Normal file
71
components/JobSkillSearchFilterBar/index.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import React, { useEffect, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { skillGroups } from "~utils/skillGroups"
|
||||
|
||||
import "./index.scss"
|
||||
|
||||
interface Props {
|
||||
sendFilters: (filters: { [key: string]: number }) => void
|
||||
}
|
||||
|
||||
const JobSkillSearchFilterBar = (props: Props) => {
|
||||
// Set up translation
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
const [currentGroup, setCurrentGroup] = useState(-1)
|
||||
|
||||
function onChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
setCurrentGroup(parseInt(event.target.value))
|
||||
}
|
||||
|
||||
function onBlur(event: React.ChangeEvent<HTMLSelectElement>) {}
|
||||
|
||||
function sendFilters() {
|
||||
const filters = {
|
||||
group: currentGroup,
|
||||
}
|
||||
|
||||
props.sendFilters(filters)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
sendFilters()
|
||||
}, [currentGroup])
|
||||
|
||||
return (
|
||||
<div className="SearchFilterBar">
|
||||
<select
|
||||
key="job-skill-groups"
|
||||
value={currentGroup}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
>
|
||||
<option key="all" value={-1}>
|
||||
{t(`job_skills.all`)}
|
||||
</option>
|
||||
<option key="damaging" value={2}>
|
||||
{t(`job_skills.damaging`)}
|
||||
</option>
|
||||
<option key="buffing" value={0}>
|
||||
{t(`job_skills.buffing`)}
|
||||
</option>
|
||||
<option key="debuffing" value={1}>
|
||||
{t(`job_skills.debuffing`)}
|
||||
</option>
|
||||
<option key="healing" value={3}>
|
||||
{t(`job_skills.healing`)}
|
||||
</option>
|
||||
<option key="emp" value={4}>
|
||||
{t(`job_skills.emp`)}
|
||||
</option>
|
||||
<option key="base" value={5}>
|
||||
{t(`job_skills.base`)}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default JobSkillSearchFilterBar
|
||||
|
|
@ -42,9 +42,6 @@ const Party = (props: Props) => {
|
|||
|
||||
// Set up states
|
||||
const { party } = useSnapshot(appState)
|
||||
const jobState = party.job
|
||||
|
||||
const [job, setJob] = useState<Job>()
|
||||
const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon)
|
||||
|
||||
// Reset state on first load
|
||||
|
|
@ -54,14 +51,6 @@ const Party = (props: Props) => {
|
|||
if (props.team) storeParty(props.team)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setJob(jobState)
|
||||
}, [jobState])
|
||||
|
||||
useEffect(() => {
|
||||
jobChanged()
|
||||
}, [job])
|
||||
|
||||
// Methods: Creating a new party
|
||||
async function createParty(extra: boolean = false) {
|
||||
let body = {
|
||||
|
|
@ -89,18 +78,6 @@ const Party = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
function jobChanged() {
|
||||
if (party.id && appState.party.editable) {
|
||||
api.endpoints.parties.update(
|
||||
party.id,
|
||||
{
|
||||
party: { job_id: job ? job.id : "" },
|
||||
},
|
||||
headers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function updateDetails(name?: string, description?: string, raid?: Raid) {
|
||||
if (
|
||||
appState.party.name !== name ||
|
||||
|
|
@ -160,6 +137,8 @@ const Party = (props: Props) => {
|
|||
appState.party.description = party.description
|
||||
appState.party.raid = party.raid
|
||||
appState.party.updated_at = party.updated_at
|
||||
appState.party.job = party.job
|
||||
appState.party.jobSkills = party.job_skills
|
||||
|
||||
appState.party.id = party.id
|
||||
appState.party.extra = party.extra
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import React, { useEffect, useRef, useState } from "react"
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { getCookie, setCookie } from "cookies-next"
|
||||
import { useRouter } from "next/router"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import InfiniteScroll from "react-infinite-scroll-component"
|
||||
|
||||
import { appState } from "~utils/appState"
|
||||
import api from "~utils/api"
|
||||
|
||||
import * as Dialog from "@radix-ui/react-dialog"
|
||||
|
|
@ -13,27 +11,29 @@ import * as Dialog from "@radix-ui/react-dialog"
|
|||
import CharacterSearchFilterBar from "~components/CharacterSearchFilterBar"
|
||||
import WeaponSearchFilterBar from "~components/WeaponSearchFilterBar"
|
||||
import SummonSearchFilterBar from "~components/SummonSearchFilterBar"
|
||||
import JobSkillSearchFilterBar from "~components/JobSkillSearchFilterBar"
|
||||
|
||||
import CharacterResult from "~components/CharacterResult"
|
||||
import WeaponResult from "~components/WeaponResult"
|
||||
import SummonResult from "~components/SummonResult"
|
||||
import JobSkillResult from "~components/JobSkillResult"
|
||||
|
||||
import type { SearchableObject, SearchableObjectArray } from "~types"
|
||||
|
||||
import "./index.scss"
|
||||
import CrossIcon from "~public/icons/Cross.svg"
|
||||
import cloneDeep from "lodash.clonedeep"
|
||||
|
||||
interface Props {
|
||||
send: (object: Character | Weapon | Summon, position: number) => any
|
||||
send: (object: SearchableObject, position: number) => any
|
||||
placeholderText: string
|
||||
fromPosition: number
|
||||
object: "weapons" | "characters" | "summons"
|
||||
job?: Job
|
||||
object: "weapons" | "characters" | "summons" | "job_skills"
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const SearchModal = (props: Props) => {
|
||||
// Set up snapshot of app state
|
||||
let { grid, search } = useSnapshot(appState)
|
||||
|
||||
// Set up router
|
||||
const router = useRouter()
|
||||
const locale = router.locale
|
||||
|
|
@ -45,23 +45,16 @@ const SearchModal = (props: Props) => {
|
|||
let scrollContainer = React.createRef<HTMLDivElement>()
|
||||
|
||||
const [firstLoad, setFirstLoad] = useState(true)
|
||||
const [objects, setObjects] = useState<{
|
||||
[id: number]: GridCharacter | GridWeapon | GridSummon | undefined
|
||||
}>()
|
||||
const [filters, setFilters] = useState<{ [key: string]: number[] }>()
|
||||
const [filters, setFilters] = useState<{ [key: string]: any }>()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState("")
|
||||
const [results, setResults] = useState<(Weapon | Summon | Character)[]>([])
|
||||
const [results, setResults] = useState<SearchableObjectArray>([])
|
||||
|
||||
// Pagination states
|
||||
const [recordCount, setRecordCount] = useState(0)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
|
||||
useEffect(() => {
|
||||
setObjects(grid[props.object])
|
||||
}, [grid, props.object])
|
||||
|
||||
useEffect(() => {
|
||||
if (searchInput.current) searchInput.current.focus()
|
||||
}, [searchInput])
|
||||
|
|
@ -80,6 +73,7 @@ const SearchModal = (props: Props) => {
|
|||
.search({
|
||||
object: props.object,
|
||||
query: query,
|
||||
job: props.job?.id,
|
||||
filters: filters,
|
||||
locale: locale,
|
||||
page: currentPage,
|
||||
|
|
@ -99,10 +93,7 @@ const SearchModal = (props: Props) => {
|
|||
})
|
||||
}
|
||||
|
||||
function replaceResults(
|
||||
count: number,
|
||||
list: Weapon[] | Summon[] | Character[]
|
||||
) {
|
||||
function replaceResults(count: number, list: SearchableObjectArray) {
|
||||
if (count > 0) {
|
||||
setResults(list)
|
||||
} else {
|
||||
|
|
@ -110,26 +101,36 @@ const SearchModal = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
function appendResults(list: Weapon[] | Summon[] | Character[]) {
|
||||
function appendResults(list: SearchableObjectArray) {
|
||||
setResults([...results, ...list])
|
||||
}
|
||||
|
||||
function storeRecentResult(result: Character | Weapon | Summon) {
|
||||
function storeRecentResult(result: SearchableObject) {
|
||||
const key = `recent_${props.object}`
|
||||
const cookie = getCookie(key)
|
||||
const cookieObj: Character[] | Weapon[] | Summon[] = cookie
|
||||
const cookieObj: SearchableObjectArray = cookie
|
||||
? JSON.parse(cookie as string)
|
||||
: []
|
||||
let recents: Character[] | Weapon[] | Summon[] = []
|
||||
let recents: SearchableObjectArray = []
|
||||
|
||||
if (props.object === "weapons") {
|
||||
recents = cloneDeep(cookieObj as Weapon[]) || []
|
||||
if (!recents.find((item) => item.granblue_id === result.granblue_id)) {
|
||||
if (
|
||||
!recents.find(
|
||||
(item) =>
|
||||
(item as Weapon).granblue_id === (result as Weapon).granblue_id
|
||||
)
|
||||
) {
|
||||
recents.unshift(result as Weapon)
|
||||
}
|
||||
} else if (props.object === "summons") {
|
||||
recents = cloneDeep(cookieObj as Summon[]) || []
|
||||
if (!recents.find((item) => item.granblue_id === result.granblue_id)) {
|
||||
if (
|
||||
!recents.find(
|
||||
(item) =>
|
||||
(item as Summon).granblue_id === (result as Summon).granblue_id
|
||||
)
|
||||
) {
|
||||
recents.unshift(result as Summon)
|
||||
}
|
||||
}
|
||||
|
|
@ -139,12 +140,12 @@ const SearchModal = (props: Props) => {
|
|||
sendData(result)
|
||||
}
|
||||
|
||||
function sendData(result: Character | Weapon | Summon) {
|
||||
function sendData(result: SearchableObject) {
|
||||
props.send(result, props.fromPosition)
|
||||
openChange()
|
||||
}
|
||||
|
||||
function receiveFilters(filters: { [key: string]: number[] }) {
|
||||
function receiveFilters(filters: { [key: string]: any }) {
|
||||
setCurrentPage(1)
|
||||
setResults([])
|
||||
setFilters(filters)
|
||||
|
|
@ -200,6 +201,9 @@ const SearchModal = (props: Props) => {
|
|||
case "characters":
|
||||
jsx = renderCharacterSearchResults(results)
|
||||
break
|
||||
case "job_skills":
|
||||
jsx = renderJobSkillSearchResults(results)
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -278,6 +282,27 @@ const SearchModal = (props: Props) => {
|
|||
return jsx
|
||||
}
|
||||
|
||||
function renderJobSkillSearchResults(results: { [key: string]: any }) {
|
||||
let jsx: React.ReactNode
|
||||
|
||||
const castResults: JobSkill[] = results as JobSkill[]
|
||||
if (castResults && Object.keys(castResults).length > 0) {
|
||||
jsx = castResults.map((result: JobSkill) => {
|
||||
return (
|
||||
<JobSkillResult
|
||||
key={result.id}
|
||||
data={result}
|
||||
onClick={() => {
|
||||
storeRecentResult(result)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return jsx
|
||||
}
|
||||
|
||||
function openChange() {
|
||||
if (open) {
|
||||
setQuery("")
|
||||
|
|
@ -330,6 +355,11 @@ const SearchModal = (props: Props) => {
|
|||
) : (
|
||||
""
|
||||
)}
|
||||
{props.object === "job_skills" ? (
|
||||
<JobSkillSearchFilterBar sendFilters={receiveFilters} />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div id="Results" ref={scrollContainer}>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import ExtraSummons from "~components/ExtraSummons"
|
|||
|
||||
import api from "~utils/api"
|
||||
import { appState } from "~utils/appState"
|
||||
import type { SearchableObject } from "~types"
|
||||
|
||||
import "./index.scss"
|
||||
|
||||
|
|
@ -83,10 +84,7 @@ const SummonGrid = (props: Props) => {
|
|||
])
|
||||
|
||||
// Methods: Adding an object from search
|
||||
function receiveSummonFromSearch(
|
||||
object: Character | Weapon | Summon,
|
||||
position: number
|
||||
) {
|
||||
function receiveSummonFromSearch(object: SearchableObject, position: number) {
|
||||
const summon = object as Summon
|
||||
|
||||
if (!party.id) {
|
||||
|
|
|
|||
|
|
@ -1,120 +1,143 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import classnames from 'classnames'
|
||||
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'
|
||||
import SummonHovercard from '~components/SummonHovercard'
|
||||
import UncapIndicator from '~components/UncapIndicator'
|
||||
import PlusIcon from '~public/icons/Add.svg'
|
||||
import SearchModal from "~components/SearchModal"
|
||||
import SummonHovercard from "~components/SummonHovercard"
|
||||
import UncapIndicator from "~components/UncapIndicator"
|
||||
import PlusIcon from "~public/icons/Add.svg"
|
||||
|
||||
import './index.scss'
|
||||
import type { SearchableObject } from "~types"
|
||||
|
||||
import "./index.scss"
|
||||
|
||||
interface Props {
|
||||
gridSummon: GridSummon | undefined
|
||||
unitType: 0 | 1 | 2
|
||||
position: number
|
||||
editable: boolean
|
||||
updateObject: (object: Character | Weapon | Summon, position: number) => void
|
||||
updateUncap: (id: string, position: number, uncap: number) => void
|
||||
gridSummon: GridSummon | undefined
|
||||
unitType: 0 | 1 | 2
|
||||
position: number
|
||||
editable: boolean
|
||||
updateObject: (object: SearchableObject, position: number) => void
|
||||
updateUncap: (id: string, position: number, uncap: number) => void
|
||||
}
|
||||
|
||||
const SummonUnit = (props: Props) => {
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const [imageUrl, setImageUrl] = useState('')
|
||||
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({
|
||||
SummonUnit: true,
|
||||
'main': props.unitType == 0,
|
||||
'grid': props.unitType == 1,
|
||||
'friend': props.unitType == 2,
|
||||
'editable': props.editable,
|
||||
'filled': (props.gridSummon !== undefined)
|
||||
})
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
|
||||
|
||||
const gridSummon = props.gridSummon
|
||||
const summon = gridSummon?.object
|
||||
const classes = classnames({
|
||||
SummonUnit: true,
|
||||
main: props.unitType == 0,
|
||||
grid: props.unitType == 1,
|
||||
friend: props.unitType == 2,
|
||||
editable: props.editable,
|
||||
filled: props.gridSummon !== undefined,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
generateImageUrl()
|
||||
})
|
||||
const gridSummon = props.gridSummon
|
||||
const summon = gridSummon?.object
|
||||
|
||||
function generateImageUrl() {
|
||||
let imgSrc = ""
|
||||
if (props.gridSummon) {
|
||||
const summon = props.gridSummon.object!
|
||||
useEffect(() => {
|
||||
generateImageUrl()
|
||||
})
|
||||
|
||||
const upgradedSummons = [
|
||||
'2040094000', '2040100000', '2040080000', '2040098000',
|
||||
'2040090000', '2040084000', '2040003000', '2040056000',
|
||||
'2040020000', '2040034000', '2040028000', '2040027000',
|
||||
'2040046000', '2040047000'
|
||||
]
|
||||
|
||||
let suffix = ''
|
||||
if (upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 && props.gridSummon.uncap_level == 5)
|
||||
suffix = '_02'
|
||||
|
||||
// Generate the correct source for the summon
|
||||
if (props.unitType == 0 || props.unitType == 2)
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-main/${summon.granblue_id}${suffix}.jpg`
|
||||
else
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`
|
||||
}
|
||||
|
||||
setImageUrl(imgSrc)
|
||||
function generateImageUrl() {
|
||||
let imgSrc = ""
|
||||
if (props.gridSummon) {
|
||||
const summon = props.gridSummon.object!
|
||||
|
||||
const upgradedSummons = [
|
||||
"2040094000",
|
||||
"2040100000",
|
||||
"2040080000",
|
||||
"2040098000",
|
||||
"2040090000",
|
||||
"2040084000",
|
||||
"2040003000",
|
||||
"2040056000",
|
||||
"2040020000",
|
||||
"2040034000",
|
||||
"2040028000",
|
||||
"2040027000",
|
||||
"2040046000",
|
||||
"2040047000",
|
||||
]
|
||||
|
||||
let suffix = ""
|
||||
if (
|
||||
upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 &&
|
||||
props.gridSummon.uncap_level == 5
|
||||
)
|
||||
suffix = "_02"
|
||||
|
||||
// Generate the correct source for the summon
|
||||
if (props.unitType == 0 || props.unitType == 2)
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-main/${summon.granblue_id}${suffix}.jpg`
|
||||
else
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`
|
||||
}
|
||||
|
||||
function passUncapData(uncap: number) {
|
||||
if (props.gridSummon)
|
||||
props.updateUncap(props.gridSummon.id, props.position, uncap)
|
||||
}
|
||||
setImageUrl(imgSrc)
|
||||
}
|
||||
|
||||
const image = (
|
||||
<div className="SummonImage">
|
||||
<img alt={summon?.name.en} className="grid_image" src={imageUrl} />
|
||||
{ (props.editable) ? <span className='icon'><PlusIcon /></span> : '' }
|
||||
</div>
|
||||
)
|
||||
function passUncapData(uncap: number) {
|
||||
if (props.gridSummon)
|
||||
props.updateUncap(props.gridSummon.id, props.position, uncap)
|
||||
}
|
||||
|
||||
const editableImage = (
|
||||
<SearchModal
|
||||
placeholderText={t('search.placeholders.summon')}
|
||||
fromPosition={props.position}
|
||||
object="summons"
|
||||
send={props.updateObject}>
|
||||
{image}
|
||||
</SearchModal>
|
||||
)
|
||||
const image = (
|
||||
<div className="SummonImage">
|
||||
<img alt={summon?.name.en} className="grid_image" src={imageUrl} />
|
||||
{props.editable ? (
|
||||
<span className="icon">
|
||||
<PlusIcon />
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const unitContent = (
|
||||
<div className={classes}>
|
||||
{ (props.editable) ? editableImage : image }
|
||||
{ (gridSummon) ?
|
||||
<UncapIndicator
|
||||
type="summon"
|
||||
ulb={gridSummon.object.uncap.ulb || false}
|
||||
flb={gridSummon.object.uncap.flb || false}
|
||||
uncapLevel={gridSummon.uncap_level}
|
||||
updateUncap={passUncapData}
|
||||
special={false}
|
||||
/> : ''
|
||||
}
|
||||
<h3 className="SummonName">{summon?.name[locale]}</h3>
|
||||
</div>
|
||||
)
|
||||
const editableImage = (
|
||||
<SearchModal
|
||||
placeholderText={t("search.placeholders.summon")}
|
||||
fromPosition={props.position}
|
||||
object="summons"
|
||||
send={props.updateObject}
|
||||
>
|
||||
{image}
|
||||
</SearchModal>
|
||||
)
|
||||
|
||||
const withHovercard = (
|
||||
<SummonHovercard gridSummon={gridSummon!}>
|
||||
{unitContent}
|
||||
</SummonHovercard>
|
||||
)
|
||||
const unitContent = (
|
||||
<div className={classes}>
|
||||
{props.editable ? editableImage : image}
|
||||
{gridSummon ? (
|
||||
<UncapIndicator
|
||||
type="summon"
|
||||
ulb={gridSummon.object.uncap.ulb || false}
|
||||
flb={gridSummon.object.uncap.flb || false}
|
||||
uncapLevel={gridSummon.uncap_level}
|
||||
updateUncap={passUncapData}
|
||||
special={false}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<h3 className="SummonName">{summon?.name[locale]}</h3>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (gridSummon && !props.editable) ? withHovercard : unitContent
|
||||
const withHovercard = (
|
||||
<SummonHovercard gridSummon={gridSummon!}>{unitContent}</SummonHovercard>
|
||||
)
|
||||
|
||||
return gridSummon && !props.editable ? withHovercard : unitContent
|
||||
}
|
||||
|
||||
export default SummonUnit
|
||||
|
|
|
|||
|
|
@ -1,101 +1,129 @@
|
|||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import UncapStar from '~components/UncapStar'
|
||||
import React, { useEffect, useRef, useState } from "react"
|
||||
import UncapStar from "~components/UncapStar"
|
||||
|
||||
import './index.scss'
|
||||
import "./index.scss"
|
||||
|
||||
interface Props {
|
||||
type: 'character' | 'weapon' | 'summon'
|
||||
rarity?: number
|
||||
uncapLevel?: number
|
||||
flb: boolean
|
||||
ulb: boolean
|
||||
special: boolean
|
||||
updateUncap?: (uncap: number) => void
|
||||
type: "character" | "weapon" | "summon"
|
||||
rarity?: number
|
||||
uncapLevel?: number
|
||||
flb: boolean
|
||||
ulb: boolean
|
||||
special: boolean
|
||||
updateUncap?: (uncap: number) => void
|
||||
}
|
||||
|
||||
const UncapIndicator = (props: Props) => {
|
||||
const [uncap, setUncap] = useState(props.uncapLevel)
|
||||
const [uncap, setUncap] = useState(props.uncapLevel)
|
||||
|
||||
const numStars = setNumStars()
|
||||
function setNumStars() {
|
||||
let numStars
|
||||
|
||||
if (props.type === 'character') {
|
||||
if (props.special) {
|
||||
if (props.ulb) {
|
||||
numStars = 5
|
||||
} else if (props.flb) {
|
||||
numStars = 4
|
||||
} else {
|
||||
numStars = 3
|
||||
}
|
||||
} else {
|
||||
if (props.ulb) {
|
||||
numStars = 6
|
||||
} else if (props.flb) {
|
||||
numStars = 5
|
||||
} else {
|
||||
numStars = 4
|
||||
}
|
||||
}
|
||||
const numStars = setNumStars()
|
||||
function setNumStars() {
|
||||
let numStars
|
||||
|
||||
if (props.type === "character") {
|
||||
if (props.special) {
|
||||
if (props.ulb) {
|
||||
numStars = 5
|
||||
} else if (props.flb) {
|
||||
numStars = 4
|
||||
} else {
|
||||
if (props.ulb) {
|
||||
numStars = 5
|
||||
} else if (props.flb) {
|
||||
numStars = 4
|
||||
} else {
|
||||
numStars = 3
|
||||
}
|
||||
numStars = 3
|
||||
}
|
||||
|
||||
return numStars
|
||||
}
|
||||
|
||||
function toggleStar(index: number, empty: boolean) {
|
||||
if (props.updateUncap) {
|
||||
if (empty) props.updateUncap(index + 1)
|
||||
else props.updateUncap(index)
|
||||
} else {
|
||||
if (props.ulb) {
|
||||
numStars = 6
|
||||
} else if (props.flb) {
|
||||
numStars = 5
|
||||
} else {
|
||||
numStars = 4
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (props.ulb) {
|
||||
numStars = 5
|
||||
} else if (props.flb) {
|
||||
numStars = 4
|
||||
} else {
|
||||
numStars = 3
|
||||
}
|
||||
}
|
||||
|
||||
const transcendence = (i: number) => {
|
||||
return <UncapStar ulb={true} empty={ (props.uncapLevel) ? i >= props.uncapLevel : false } key={`star_${i}`} index={i} onClick={toggleStar} />
|
||||
}
|
||||
return numStars
|
||||
}
|
||||
|
||||
const ulb = (i: number) => {
|
||||
return <UncapStar ulb={true} empty={ (props.uncapLevel) ? i >= props.uncapLevel : false } key={`star_${i}`} index={i} onClick={toggleStar} />
|
||||
}
|
||||
|
||||
const flb = (i: number) => {
|
||||
return <UncapStar flb={true} empty={ (props.uncapLevel) ? i >= props.uncapLevel : false } key={`star_${i}`} index={i} onClick={toggleStar} />
|
||||
}
|
||||
|
||||
const mlb = (i: number) => {
|
||||
// console.log("MLB; Number of stars:", props.uncapLevel)
|
||||
return <UncapStar empty={ (props.uncapLevel) ? i >= props.uncapLevel : false } key={`star_${i}`} index={i} onClick={toggleStar} />
|
||||
function toggleStar(index: number, empty: boolean) {
|
||||
if (props.updateUncap) {
|
||||
if (empty) props.updateUncap(index + 1)
|
||||
else props.updateUncap(index)
|
||||
}
|
||||
}
|
||||
|
||||
const transcendence = (i: number) => {
|
||||
return (
|
||||
<ul className="UncapIndicator">
|
||||
{
|
||||
Array.from(Array(numStars)).map((x, i) => {
|
||||
if (props.type === 'character' && i > 4) {
|
||||
if (props.special)
|
||||
return ulb(i)
|
||||
else
|
||||
return transcendence(i)
|
||||
} else if (
|
||||
props.special && props.type === 'character' && i == 3 ||
|
||||
props.type === 'character' && i == 4 ||
|
||||
props.type !== 'character' && i > 2) {
|
||||
return flb(i)
|
||||
} else {
|
||||
return mlb(i)
|
||||
}
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
<UncapStar
|
||||
ulb={true}
|
||||
empty={props.uncapLevel ? i >= props.uncapLevel : false}
|
||||
key={`star_${i}`}
|
||||
index={i}
|
||||
onClick={toggleStar}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ulb = (i: number) => {
|
||||
return (
|
||||
<UncapStar
|
||||
ulb={true}
|
||||
empty={props.uncapLevel ? i >= props.uncapLevel : false}
|
||||
key={`star_${i}`}
|
||||
index={i}
|
||||
onClick={toggleStar}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const flb = (i: number) => {
|
||||
return (
|
||||
<UncapStar
|
||||
flb={true}
|
||||
empty={props.uncapLevel ? i >= props.uncapLevel : false}
|
||||
key={`star_${i}`}
|
||||
index={i}
|
||||
onClick={toggleStar}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const mlb = (i: number) => {
|
||||
// console.log("MLB; Number of stars:", props.uncapLevel)
|
||||
return (
|
||||
<UncapStar
|
||||
empty={props.uncapLevel ? i >= props.uncapLevel : false}
|
||||
key={`star_${i}`}
|
||||
index={i}
|
||||
onClick={toggleStar}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="UncapIndicator">
|
||||
{Array.from(Array(numStars)).map((x, i) => {
|
||||
if (props.type === "character" && i > 4) {
|
||||
if (props.special) return ulb(i)
|
||||
else return transcendence(i)
|
||||
} else if (
|
||||
(props.special && props.type === "character" && i == 3) ||
|
||||
(props.type === "character" && i == 4) ||
|
||||
(props.type !== "character" && i > 2)
|
||||
) {
|
||||
return flb(i)
|
||||
} else {
|
||||
return mlb(i)
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default UncapIndicator
|
||||
export default UncapIndicator
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import ExtraWeapons from "~components/ExtraWeapons"
|
|||
import api from "~utils/api"
|
||||
import { appState } from "~utils/appState"
|
||||
|
||||
import type { SearchableObject } from "~types"
|
||||
|
||||
import "./index.scss"
|
||||
|
||||
// Props
|
||||
|
|
@ -71,10 +73,7 @@ const WeaponGrid = (props: Props) => {
|
|||
}, [appState.grid.weapons.mainWeapon, appState.grid.weapons.allWeapons])
|
||||
|
||||
// Methods: Adding an object from search
|
||||
function receiveWeaponFromSearch(
|
||||
object: Character | Weapon | Summon,
|
||||
position: number
|
||||
) {
|
||||
function receiveWeaponFromSearch(object: SearchableObject, position: number) {
|
||||
const weapon = object as Weapon
|
||||
if (position == 1) appState.party.element = weapon.element
|
||||
|
||||
|
|
|
|||
|
|
@ -1,131 +1,145 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import classnames from 'classnames'
|
||||
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'
|
||||
import WeaponModal from '~components/WeaponModal'
|
||||
import WeaponHovercard from '~components/WeaponHovercard'
|
||||
import UncapIndicator from '~components/UncapIndicator'
|
||||
import Button from '~components/Button'
|
||||
import SearchModal from "~components/SearchModal"
|
||||
import WeaponModal from "~components/WeaponModal"
|
||||
import WeaponHovercard from "~components/WeaponHovercard"
|
||||
import UncapIndicator from "~components/UncapIndicator"
|
||||
import Button from "~components/Button"
|
||||
|
||||
import { ButtonType } from '~utils/enums'
|
||||
import { ButtonType } from "~utils/enums"
|
||||
import type { SearchableObject } from "~types"
|
||||
|
||||
import PlusIcon from '~public/icons/Add.svg'
|
||||
import './index.scss'
|
||||
import PlusIcon from "~public/icons/Add.svg"
|
||||
import "./index.scss"
|
||||
|
||||
interface Props {
|
||||
gridWeapon: GridWeapon | undefined
|
||||
unitType: 0 | 1
|
||||
position: number
|
||||
editable: boolean
|
||||
updateObject: (object: Character | Weapon | Summon, position: number) => void
|
||||
updateUncap: (id: string, position: number, uncap: number) => void
|
||||
gridWeapon: GridWeapon | undefined
|
||||
unitType: 0 | 1
|
||||
position: number
|
||||
editable: boolean
|
||||
updateObject: (object: SearchableObject, position: number) => void
|
||||
updateUncap: (id: string, position: number, uncap: number) => void
|
||||
}
|
||||
|
||||
const WeaponUnit = (props: Props) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
const [imageUrl, setImageUrl] = useState('')
|
||||
const [imageUrl, setImageUrl] = useState("")
|
||||
|
||||
const router = useRouter()
|
||||
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
|
||||
|
||||
const classes = classnames({
|
||||
WeaponUnit: true,
|
||||
'mainhand': props.unitType == 0,
|
||||
'grid': props.unitType == 1,
|
||||
'editable': props.editable,
|
||||
'filled': (props.gridWeapon !== undefined)
|
||||
})
|
||||
const classes = classnames({
|
||||
WeaponUnit: true,
|
||||
mainhand: props.unitType == 0,
|
||||
grid: props.unitType == 1,
|
||||
editable: props.editable,
|
||||
filled: props.gridWeapon !== undefined,
|
||||
})
|
||||
|
||||
const gridWeapon = props.gridWeapon
|
||||
const weapon = gridWeapon?.object
|
||||
const gridWeapon = props.gridWeapon
|
||||
const weapon = gridWeapon?.object
|
||||
|
||||
useEffect(() => {
|
||||
generateImageUrl()
|
||||
})
|
||||
useEffect(() => {
|
||||
generateImageUrl()
|
||||
})
|
||||
|
||||
function generateImageUrl() {
|
||||
let imgSrc = ""
|
||||
if (props.gridWeapon) {
|
||||
const weapon = props.gridWeapon.object!
|
||||
|
||||
if (props.unitType == 0) {
|
||||
if (props.gridWeapon.object.element == 0 && props.gridWeapon.element)
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}_${props.gridWeapon.element}.jpg`
|
||||
else
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg`
|
||||
} else {
|
||||
if (props.gridWeapon.object.element == 0 && props.gridWeapon.element)
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${props.gridWeapon.element}.jpg`
|
||||
else
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`
|
||||
}
|
||||
}
|
||||
|
||||
setImageUrl(imgSrc)
|
||||
function generateImageUrl() {
|
||||
let imgSrc = ""
|
||||
if (props.gridWeapon) {
|
||||
const weapon = props.gridWeapon.object!
|
||||
|
||||
if (props.unitType == 0) {
|
||||
if (props.gridWeapon.object.element == 0 && props.gridWeapon.element)
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}_${props.gridWeapon.element}.jpg`
|
||||
else
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg`
|
||||
} else {
|
||||
if (props.gridWeapon.object.element == 0 && props.gridWeapon.element)
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${props.gridWeapon.element}.jpg`
|
||||
else
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`
|
||||
}
|
||||
}
|
||||
|
||||
function passUncapData(uncap: number) {
|
||||
if (props.gridWeapon)
|
||||
props.updateUncap(props.gridWeapon.id, props.position, uncap)
|
||||
}
|
||||
setImageUrl(imgSrc)
|
||||
}
|
||||
|
||||
function canBeModified(gridWeapon: GridWeapon) {
|
||||
const weapon = gridWeapon.object
|
||||
function passUncapData(uncap: number) {
|
||||
if (props.gridWeapon)
|
||||
props.updateUncap(props.gridWeapon.id, props.position, uncap)
|
||||
}
|
||||
|
||||
return weapon.ax > 0 ||
|
||||
(weapon.series) && [2, 3, 17, 22, 24].includes(weapon.series)
|
||||
}
|
||||
function canBeModified(gridWeapon: GridWeapon) {
|
||||
const weapon = gridWeapon.object
|
||||
|
||||
const image = (
|
||||
<div className="WeaponImage">
|
||||
<img alt={weapon?.name.en} className="grid_image" src={imageUrl} />
|
||||
{ (props.editable) ? <span className='icon'><PlusIcon /></span> : '' }
|
||||
</div>
|
||||
return (
|
||||
weapon.ax > 0 ||
|
||||
(weapon.series && [2, 3, 17, 22, 24].includes(weapon.series))
|
||||
)
|
||||
}
|
||||
|
||||
const editableImage = (
|
||||
<SearchModal
|
||||
placeholderText={t('search.placeholders.weapon')}
|
||||
fromPosition={props.position}
|
||||
object="weapons"
|
||||
send={props.updateObject}>
|
||||
{image}
|
||||
</SearchModal>
|
||||
)
|
||||
const image = (
|
||||
<div className="WeaponImage">
|
||||
<img alt={weapon?.name.en} className="grid_image" src={imageUrl} />
|
||||
{props.editable ? (
|
||||
<span className="icon">
|
||||
<PlusIcon />
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const unitContent = (
|
||||
<div className={classes}>
|
||||
{ (props.editable && gridWeapon && canBeModified(gridWeapon)) ?
|
||||
<WeaponModal gridWeapon={gridWeapon}>
|
||||
<div>
|
||||
<Button icon="settings" type={ButtonType.IconOnly}/>
|
||||
</div>
|
||||
</WeaponModal>: '' }
|
||||
{ (props.editable) ? editableImage : image }
|
||||
{ (gridWeapon) ?
|
||||
<UncapIndicator
|
||||
type="weapon"
|
||||
ulb={gridWeapon.object.uncap.ulb || false}
|
||||
flb={gridWeapon.object.uncap.flb || false}
|
||||
uncapLevel={gridWeapon.uncap_level}
|
||||
updateUncap={passUncapData}
|
||||
special={false}
|
||||
/> : ''
|
||||
}
|
||||
<h3 className="WeaponName">{weapon?.name[locale]}</h3>
|
||||
</div>
|
||||
)
|
||||
const editableImage = (
|
||||
<SearchModal
|
||||
placeholderText={t("search.placeholders.weapon")}
|
||||
fromPosition={props.position}
|
||||
object="weapons"
|
||||
send={props.updateObject}
|
||||
>
|
||||
{image}
|
||||
</SearchModal>
|
||||
)
|
||||
|
||||
const withHovercard = (
|
||||
<WeaponHovercard gridWeapon={gridWeapon!}>
|
||||
{unitContent}
|
||||
</WeaponHovercard>
|
||||
)
|
||||
const unitContent = (
|
||||
<div className={classes}>
|
||||
{props.editable && gridWeapon && canBeModified(gridWeapon) ? (
|
||||
<WeaponModal gridWeapon={gridWeapon}>
|
||||
<div>
|
||||
<Button icon="settings" type={ButtonType.IconOnly} />
|
||||
</div>
|
||||
</WeaponModal>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{props.editable ? editableImage : image}
|
||||
{gridWeapon ? (
|
||||
<UncapIndicator
|
||||
type="weapon"
|
||||
ulb={gridWeapon.object.uncap.ulb || false}
|
||||
flb={gridWeapon.object.uncap.flb || false}
|
||||
uncapLevel={gridWeapon.uncap_level}
|
||||
updateUncap={passUncapData}
|
||||
special={false}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<h3 className="WeaponName">{weapon?.name[locale]}</h3>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (gridWeapon && !props.editable) ? withHovercard : unitContent
|
||||
const withHovercard = (
|
||||
<WeaponHovercard gridWeapon={gridWeapon!}>{unitContent}</WeaponHovercard>
|
||||
)
|
||||
|
||||
return gridWeapon && !props.editable ? withHovercard : unitContent
|
||||
}
|
||||
|
||||
export default WeaponUnit
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import React from "react"
|
||||
import React, { useEffect } from "react"
|
||||
import { getCookie } from "cookies-next"
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations"
|
||||
|
||||
import Party from "~components/Party"
|
||||
|
||||
import { appState } from "~utils/appState"
|
||||
import api from "~utils/api"
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
interface Props {
|
||||
jobs: Job[]
|
||||
jobSkills: JobSkill[]
|
||||
raids: Raid[]
|
||||
sortedRaids: Raid[][]
|
||||
}
|
||||
|
|
@ -18,6 +22,16 @@ const NewRoute: React.FC<Props> = (props: Props) => {
|
|||
window.history.replaceState(null, `Grid Tool`, `${path}`)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
persistStaticData()
|
||||
}, [persistStaticData])
|
||||
|
||||
function persistStaticData() {
|
||||
appState.raids = props.raids
|
||||
appState.jobs = props.jobs
|
||||
appState.jobSkills = props.jobSkills
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="Content">
|
||||
<Party new={true} raids={props.sortedRaids} pushHistory={callback} />
|
||||
|
|
@ -51,8 +65,17 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
.getAll({ params: headers })
|
||||
.then((response) => organizeRaids(response.data.map((r: any) => r.raid)))
|
||||
|
||||
let jobs = await api.endpoints.jobs
|
||||
.getAll({ params: headers })
|
||||
.then((response) => { return response.data })
|
||||
|
||||
let jobSkills = await api.allSkills(headers)
|
||||
.then((response) => { return response.data })
|
||||
|
||||
return {
|
||||
props: {
|
||||
jobs: jobs,
|
||||
jobSkills: jobSkills,
|
||||
raids: raids,
|
||||
sortedRaids: sortedRaids,
|
||||
...(await serverSideTranslations(locale, ["common"])),
|
||||
|
|
|
|||
|
|
@ -1,20 +1,33 @@
|
|||
import React from "react"
|
||||
import React, { useEffect } from "react"
|
||||
import { getCookie } from "cookies-next"
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations"
|
||||
|
||||
import Party from "~components/Party"
|
||||
|
||||
import { appState } from "~utils/appState"
|
||||
import api from "~utils/api"
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
interface Props {
|
||||
party: Party
|
||||
jobs: Job[]
|
||||
jobSkills: JobSkill[]
|
||||
raids: Raid[]
|
||||
sortedRaids: Raid[][]
|
||||
}
|
||||
|
||||
const PartyRoute: React.FC<Props> = (props: Props) => {
|
||||
useEffect(() => {
|
||||
persistStaticData()
|
||||
}, [persistStaticData])
|
||||
|
||||
function persistStaticData() {
|
||||
appState.raids = props.raids
|
||||
appState.jobs = props.jobs
|
||||
appState.jobSkills = props.jobSkills
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="Content">
|
||||
<Party team={props.party} raids={props.sortedRaids} />
|
||||
|
|
@ -48,6 +61,16 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
.getAll()
|
||||
.then((response) => organizeRaids(response.data.map((r: any) => r.raid)))
|
||||
|
||||
let jobs = await api.endpoints.jobs
|
||||
.getAll({ params: headers })
|
||||
.then((response) => {
|
||||
return response.data
|
||||
})
|
||||
|
||||
let jobSkills = await api.allSkills(headers).then((response) => {
|
||||
return response.data
|
||||
})
|
||||
|
||||
let party: Party | null = null
|
||||
if (query.party) {
|
||||
let response = await api.endpoints.parties.getOne({ id: query.party, params: headers })
|
||||
|
|
@ -59,6 +82,8 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
return {
|
||||
props: {
|
||||
party: party,
|
||||
jobs: jobs,
|
||||
jobSkills: jobSkills,
|
||||
raids: raids,
|
||||
sortedRaids: sortedRaids,
|
||||
...(await serverSideTranslations(locale, ["common"])),
|
||||
|
|
|
|||
|
|
@ -232,7 +232,8 @@
|
|||
"placeholders": {
|
||||
"weapon": "Search for a weapon...",
|
||||
"summon": "Search for a summon...",
|
||||
"character": "Search for a character..."
|
||||
"character": "Search for a character...",
|
||||
"job_skill": "Search job skills..."
|
||||
}
|
||||
},
|
||||
"teams": {
|
||||
|
|
@ -240,6 +241,19 @@
|
|||
"loading": "Loading teams...",
|
||||
"not_found": "No teams found"
|
||||
},
|
||||
"job_skills": {
|
||||
"all": "All skills",
|
||||
"buffing": "Buffing",
|
||||
"debuffing": "Debuffing",
|
||||
"damaging": "Damaging",
|
||||
"healing": "Healing",
|
||||
"emp": "Extended Mastery",
|
||||
"base": "Base Skills",
|
||||
"state": {
|
||||
"selectable": "Select a skill",
|
||||
"no_skill": "No skill"
|
||||
}
|
||||
},
|
||||
"extra_weapons": "Additional Weapons",
|
||||
"coming_soon": "Coming Soon",
|
||||
"no_title": "Untitled",
|
||||
|
|
|
|||
|
|
@ -233,7 +233,8 @@
|
|||
"placeholders": {
|
||||
"weapon": "武器を検索...",
|
||||
"summon": "召喚石を検索...",
|
||||
"character": "キャラを検索..."
|
||||
"character": "キャラを検索...",
|
||||
"job_skill": "ジョブのスキルを検索..."
|
||||
}
|
||||
},
|
||||
"teams": {
|
||||
|
|
@ -241,6 +242,19 @@
|
|||
"loading": "ロード中...",
|
||||
"not_found": "編成は見つかりませんでした"
|
||||
},
|
||||
"job_skills": {
|
||||
"all": "全てのアビリティ",
|
||||
"buffing": "強化アビリティ",
|
||||
"debuffing": "弱体アビリティ",
|
||||
"damaging": "ダメージアビリティ",
|
||||
"healing": "回復アビリティ",
|
||||
"emp": "リミットアビリティ",
|
||||
"base": "ベースアビリティ",
|
||||
"state": {
|
||||
"selectable": "アビリティを選択",
|
||||
"no_skill": "設定されていません"
|
||||
}
|
||||
},
|
||||
"extra_weapons": "Additional<br/>Weapons",
|
||||
"coming_soon": "開発中",
|
||||
"no_title": "無題",
|
||||
|
|
|
|||
31
types/Job.d.ts
vendored
31
types/Job.d.ts
vendored
|
|
@ -1,15 +1,16 @@
|
|||
interface Job {
|
||||
id: string
|
||||
row: string
|
||||
ml: boolean
|
||||
order: number
|
||||
name: {
|
||||
[key: string]: string
|
||||
en: string
|
||||
ja: string
|
||||
}
|
||||
proficiency: {
|
||||
proficiency1: number
|
||||
proficiency2: number
|
||||
}
|
||||
}
|
||||
interface Job {
|
||||
id: string
|
||||
row: string
|
||||
ml: boolean
|
||||
order: number
|
||||
name: {
|
||||
[key: string]: string
|
||||
en: string
|
||||
ja: string
|
||||
}
|
||||
proficiency: {
|
||||
proficiency1: number
|
||||
proficiency2: number
|
||||
}
|
||||
base_job?: Job
|
||||
}
|
||||
|
|
|
|||
16
types/JobSkill.d.ts
vendored
Normal file
16
types/JobSkill.d.ts
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
interface JobSkill {
|
||||
id: string
|
||||
job: Job
|
||||
name: {
|
||||
[key: string]: string
|
||||
en: string
|
||||
ja: string
|
||||
}
|
||||
slug: string
|
||||
color: number
|
||||
main: boolean
|
||||
base: boolean
|
||||
sub: boolean
|
||||
emp: boolean
|
||||
order: number
|
||||
}
|
||||
10
types/Party.d.ts
vendored
10
types/Party.d.ts
vendored
|
|
@ -1,8 +1,18 @@
|
|||
type JobSkillObject = {
|
||||
[key: number]: JobSkill | undefined
|
||||
0: JobSkill | undefined
|
||||
1: JobSkill | undefined
|
||||
2: JobSkill | undefined
|
||||
3: JobSkill | undefined
|
||||
}
|
||||
|
||||
interface Party {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
raid: Raid
|
||||
job: Job
|
||||
job_skills: JobSkillObject
|
||||
shortcode: string
|
||||
extra: boolean
|
||||
favorited: boolean
|
||||
|
|
|
|||
9
types/index.d.ts
vendored
Normal file
9
types/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export type SearchableObject = Character | Weapon | Summon | JobSkill
|
||||
export type SearchableObjectArray = (Character | Weapon | Summon | JobSkill)[]
|
||||
export type JobSkillObject = {
|
||||
[key: number]: JobSkill | undefined
|
||||
0: JobSkill | undefined
|
||||
1: JobSkill | undefined
|
||||
2: JobSkill | undefined
|
||||
3: JobSkill | undefined
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ class Api {
|
|||
url: string
|
||||
endpoints: { [key: string]: EndpointMap }
|
||||
|
||||
constructor({url}: {url: string}) {
|
||||
constructor({ url }: { url: string }) {
|
||||
this.url = url
|
||||
this.endpoints = {}
|
||||
}
|
||||
|
|
@ -56,13 +56,14 @@ class Api {
|
|||
return axios.post(`${ oauthUrl }/token`, object)
|
||||
}
|
||||
|
||||
search({ object, query, filters, locale = "en", page = 0 }:
|
||||
{ object: string, query: string, filters?: { [key: string]: number[] }, locale?: string, page?: number }) {
|
||||
search({ object, query, job, filters, locale = "en", page = 0 }:
|
||||
{ object: string, query: string, job?: string, filters?: { [key: string]: number[] }, locale?: string, page?: number }) {
|
||||
const resourceUrl = `${this.url}/${name}`
|
||||
return axios.post(`${resourceUrl}search/${object}`, {
|
||||
search: {
|
||||
query: query,
|
||||
filters: filters,
|
||||
job: job,
|
||||
locale: locale,
|
||||
page: page
|
||||
}
|
||||
|
|
@ -92,6 +93,22 @@ class Api {
|
|||
const resourceUrl = `${this.url}/characters/resolve`
|
||||
return axios.post(resourceUrl, body, { headers: params })
|
||||
}
|
||||
|
||||
updateJob({ partyId, params }: { partyId: string, params?: {} }) {
|
||||
const resourceUrl = `${this.url}/parties/${partyId}/jobs`
|
||||
return axios.put(resourceUrl, params)
|
||||
}
|
||||
|
||||
updateJobSkills({ partyId, params }: { partyId: string, params?: {} }) {
|
||||
const resourceUrl = `${this.url}/parties/${partyId}/job_skills`
|
||||
return axios.put(resourceUrl, params)
|
||||
}
|
||||
|
||||
allSkills(params: {}) {
|
||||
const resourceUrl = `${this.url}/jobs/skills`
|
||||
return axios.get(resourceUrl, params)
|
||||
}
|
||||
|
||||
savedTeams(params: {}) {
|
||||
const resourceUrl = `${this.url}/parties/favorites`
|
||||
return axios.get(resourceUrl, params)
|
||||
|
|
@ -127,15 +144,15 @@ class Api {
|
|||
}
|
||||
|
||||
const api: Api = new Api({ url: process.env.NEXT_PUBLIC_SIERO_API_URL || 'https://localhost:3000/api/v1'})
|
||||
api.createEntity( { name: 'users' })
|
||||
api.createEntity( { name: 'parties' })
|
||||
api.createEntity( { name: 'grid_weapons' })
|
||||
api.createEntity( { name: 'characters' })
|
||||
api.createEntity( { name: 'weapons' })
|
||||
api.createEntity( { name: 'summons' })
|
||||
api.createEntity( { name: 'jobs' })
|
||||
api.createEntity( { name: 'raids' })
|
||||
api.createEntity( { name: 'weapon_keys' })
|
||||
api.createEntity( { name: 'favorites' })
|
||||
api.createEntity({ name: 'users' })
|
||||
api.createEntity({ name: 'parties' })
|
||||
api.createEntity({ name: 'grid_weapons' })
|
||||
api.createEntity({ name: 'characters' })
|
||||
api.createEntity({ name: 'weapons' })
|
||||
api.createEntity({ name: 'summons' })
|
||||
api.createEntity({ name: 'jobs' })
|
||||
api.createEntity({ name: 'raids' })
|
||||
api.createEntity({ name: 'weapon_keys' })
|
||||
api.createEntity({ name: 'favorites' })
|
||||
|
||||
export default api
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { proxy } from "valtio"
|
||||
import { JobSkillObject } from "~types"
|
||||
|
||||
const emptyJob: Job = {
|
||||
id: "-1",
|
||||
|
|
@ -25,6 +26,7 @@ interface AppState {
|
|||
name: string | undefined
|
||||
description: string | undefined
|
||||
job: Job
|
||||
jobSkills: JobSkillObject
|
||||
raid: Raid | undefined
|
||||
element: number
|
||||
extra: boolean
|
||||
|
|
@ -53,6 +55,8 @@ interface AppState {
|
|||
}
|
||||
}
|
||||
raids: Raid[]
|
||||
jobs: Job[]
|
||||
jobSkills: JobSkill[]
|
||||
}
|
||||
|
||||
export const initialAppState: AppState = {
|
||||
|
|
@ -63,6 +67,12 @@ export const initialAppState: AppState = {
|
|||
name: undefined,
|
||||
description: undefined,
|
||||
job: emptyJob,
|
||||
jobSkills: {
|
||||
0: undefined,
|
||||
1: undefined,
|
||||
2: undefined,
|
||||
3: undefined,
|
||||
},
|
||||
raid: undefined,
|
||||
element: 0,
|
||||
extra: false,
|
||||
|
|
@ -91,6 +101,8 @@ export const initialAppState: AppState = {
|
|||
},
|
||||
},
|
||||
raids: [],
|
||||
jobs: [],
|
||||
jobSkills: [],
|
||||
}
|
||||
|
||||
export const appState = proxy(initialAppState)
|
||||
|
|
|
|||
91
utils/skillGroups.tsx
Normal file
91
utils/skillGroups.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
export interface SkillGroup {
|
||||
id: number
|
||||
name: {
|
||||
[key: string]: string
|
||||
en: string
|
||||
ja: string
|
||||
}
|
||||
}
|
||||
|
||||
export const skillClassification: SkillGroup[] = [
|
||||
{
|
||||
id: 0,
|
||||
name: {
|
||||
en: "Buffing",
|
||||
ja: "強化アビリティ",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: {
|
||||
en: "Debuffing",
|
||||
ja: "弱体アビリティ",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: {
|
||||
en: "Damaging",
|
||||
ja: "ダメージアビリティ",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: {
|
||||
en: "Healing",
|
||||
ja: "回復アビリティ",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: {
|
||||
en: "Field",
|
||||
ja: "フィールドアビリティ",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export const skillGroups: SkillGroup[] = [
|
||||
{
|
||||
id: 0,
|
||||
name: {
|
||||
en: "Buffing",
|
||||
ja: "強化アビリティ",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: {
|
||||
en: "Debuffing",
|
||||
ja: "弱体アビリティ",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: {
|
||||
en: "Damaging",
|
||||
ja: "ダメージアビリティ",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: {
|
||||
en: "Healing",
|
||||
ja: "回復アビリティ",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: {
|
||||
en: "Extended Mastery",
|
||||
ja: "リミットアビリティ",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: {
|
||||
en: "Base",
|
||||
ja: "ベースアビリティ",
|
||||
},
|
||||
},
|
||||
]
|
||||
Loading…
Reference in a new issue