diff --git a/.gitignore b/.gitignore index e6fbad6e..34d553d3 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/components/Alert/index.scss b/components/Alert/index.scss new file mode 100644 index 00000000..fe2e0df9 --- /dev/null +++ b/components/Alert/index.scss @@ -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; + } + } + } +} diff --git a/components/Alert/index.tsx b/components/Alert/index.tsx new file mode 100644 index 00000000..8e785c70 --- /dev/null +++ b/components/Alert/index.tsx @@ -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 ( + + + +
+ + {props.title ? Error : ""} + + {props.message} + +
+ + + + {props.primaryAction ? ( + + {props.primaryActionText} + + ) : ( + "" + )} +
+
+
+
+
+ ) +} + +export default Alert diff --git a/components/CharacterConflictModal/index.tsx b/components/CharacterConflictModal/index.tsx index 04ade165..1f1ff6c0 100644 --- a/components/CharacterConflictModal/index.tsx +++ b/components/CharacterConflictModal/index.tsx @@ -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 diff --git a/components/CharacterGrid/index.tsx b/components/CharacterGrid/index.tsx index af9b4721..521c54c2 100644 --- a/components/CharacterGrid/index.tsx +++ b/components/CharacterGrid/index.tsx @@ -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([]) const [position, setPosition] = useState(0) + // Set up state for data + const [job, setJob] = useState() + const [jobSkills, setJobSkills] = useState({ + 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 (
+ 0} + message={errorMessage} + cancelAction={cancelAlert} + cancelActionText={"Got it"} + />
- + 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 = ( -
- {character?.name.en} - { (props.editable) ? : '' } -
- ) + function passUncapData(uncap: number) { + if (props.gridCharacter) + props.updateUncap(props.gridCharacter.id, props.position, uncap) + } - const editableImage = ( - - {image} - - ) + const image = ( +
+ {character?.name.en} + {props.editable ? ( + + + + ) : ( + "" + )} +
+ ) - const unitContent = ( -
- { (props.editable) ? editableImage : image } - { (gridCharacter && character) ? - : '' } -

{character?.name[locale]}

-
- ) + const editableImage = ( + + {image} + + ) - const withHovercard = ( - - {unitContent} - - ) + const unitContent = ( +
+ {props.editable ? editableImage : image} + {gridCharacter && character ? ( + + ) : ( + "" + )} +

{character?.name[locale]}

+
+ ) - return ( - (gridCharacter && !props.editable) ? withHovercard : unitContent - ) + const withHovercard = ( + + {unitContent} + + ) + + return gridCharacter && !props.editable ? withHovercard : unitContent } export default CharacterUnit diff --git a/components/ExtraSummons/index.tsx b/components/ExtraSummons/index.tsx index 54e888e3..c064ba25 100644 --- a/components/ExtraSummons/index.tsx +++ b/components/ExtraSummons/index.tsx @@ -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 - 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 + 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 ( -
- {t('summons.subaura')} -
    - { - Array.from(Array(numSummons)).map((x, i) => { - return ( -
  • - -
  • - ) - }) - } -
-
- ) + return ( +
+ {t("summons.subaura")} +
    + {Array.from(Array(numSummons)).map((x, i) => { + return ( +
  • + +
  • + ) + })} +
+
+ ) } export default ExtraSummons diff --git a/components/JobDropdown/index.tsx b/components/JobDropdown/index.tsx index b0a6019f..179034f4 100644 --- a/components/JobDropdown/index.tsx +++ b/components/JobDropdown/index.tsx @@ -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) => void + currentJob?: string + onChange?: (job?: Job) => void + onBlur?: (event: React.ChangeEvent) => void } type GroupedJob = { [key: string]: Job[] } -const JobDropdown = React.forwardRef(function useFieldSet(props, ref) { +const JobDropdown = React.forwardRef( + 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() const [jobs, setJobs] = useState() const [sortedJobs, setSortedJobs] = useState() - // 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) { - 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 ( - - ) - }) + const options = + sortedJobs && + sortedJobs[group].length > 0 && + sortedJobs[group] + .sort((a, b) => a.order - b.order) + .map((item, i) => { + return ( + + ) + }) - const groupName = jobGroups.find(g => g.slug === group)?.name[locale] + const groupName = jobGroups.find((g) => g.slug === group)?.name[locale] - return ( - - {options} - - ) + return ( + + {options} + + ) } - + return ( - + ) -}) + } +) export default JobDropdown diff --git a/components/JobSection/index.scss b/components/JobSection/index.scss index bf1e710c..f6e20d20 100644 --- a/components/JobSection/index.scss +++ b/components/JobSection/index.scss @@ -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; } -} \ No newline at end of file + } + + .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; + } +} diff --git a/components/JobSection/index.tsx b/components/JobSection/index.tsx index 464f261f..55ea8234 100644 --- a/components/JobSection/index.tsx +++ b/components/JobSection/index.tsx @@ -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() - 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 ( -
-
- -
-
- -
- ) +interface Props { + job?: Job + jobSkills: JobSkillObject + editable: boolean + saveJob: (job: Job) => void + saveSkill: (skill: JobSkill, position: number) => void } -export default JobSection \ No newline at end of file +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() + const [imageUrl, setImageUrl] = useState("") + const [numSkills, setNumSkills] = useState(4) + const [skills, setSkills] = useState<{ [key: number]: JobSkill | undefined }>( + [] + ) + + const selectRef = React.createRef() + + 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 ( + + ) + } + + const editableSkillItem = (index: number) => { + return ( + + {skillItem(index, true)} + + ) + } + + 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 ( +
+
+ +
+
+
+ {props.editable ? ( + + ) : ( +

{party.job?.name[locale]}

+ )} + +
    + {[...Array(numSkills)].map((e, i) => ( +
  • + {canEditSkill(skills[i]) + ? editableSkillItem(i) + : skillItem(i, false)} +
  • + ))} +
+
+
+ ) +} + +export default JobSection diff --git a/components/JobSkillItem/index.scss b/components/JobSkillItem/index.scss new file mode 100644 index 00000000..539beb60 --- /dev/null +++ b/components/JobSkillItem/index.scss @@ -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; + } +} diff --git a/components/JobSkillItem/index.tsx b/components/JobSkillItem/index.tsx new file mode 100644 index 00000000..ba1ce9a0 --- /dev/null +++ b/components/JobSkillItem/index.tsx @@ -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( + ({ ...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 = ( + {props.skill.name[locale]} + ) + } else { + jsx = ( +
+ {props.editable && props.hasJob ? : ""} +
+ ) + } + + return jsx + } + + const label = () => { + let jsx: React.ReactNode + + if (props.skill) { + jsx =

{props.skill.name[locale]}

+ } else if (props.editable && props.hasJob) { + jsx =

{t("job_skills.state.selectable")}

+ } else { + jsx =

{t("job_skills.state.no_skill")}

+ } + + return jsx + } + + return ( +
+ {skillImage()} + {label()} +
+ ) + } +) + +export default JobSkillItem diff --git a/components/JobSkillResult/index.scss b/components/JobSkillResult/index.scss new file mode 100644 index 00000000..c8308b05 --- /dev/null +++ b/components/JobSkillResult/index.scss @@ -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; + } +} diff --git a/components/JobSkillResult/index.tsx b/components/JobSkillResult/index.tsx new file mode 100644 index 00000000..09c08028 --- /dev/null +++ b/components/JobSkillResult/index.tsx @@ -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() + + 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 ( +
  • + {skill.name[locale]} +
    +
    {skill.name[locale]}
    +
    + {group?.name[locale]} +
    +
    +
  • + ) +} + +export default JobSkillResult diff --git a/components/JobSkillSearchFilterBar/index.scss b/components/JobSkillSearchFilterBar/index.scss new file mode 100644 index 00000000..6ccb5e2e --- /dev/null +++ b/components/JobSkillSearchFilterBar/index.scss @@ -0,0 +1,3 @@ +.SearchFilterBar select { + background-color: $grey-90; +} diff --git a/components/JobSkillSearchFilterBar/index.tsx b/components/JobSkillSearchFilterBar/index.tsx new file mode 100644 index 00000000..1de78add --- /dev/null +++ b/components/JobSkillSearchFilterBar/index.tsx @@ -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) { + setCurrentGroup(parseInt(event.target.value)) + } + + function onBlur(event: React.ChangeEvent) {} + + function sendFilters() { + const filters = { + group: currentGroup, + } + + props.sendFilters(filters) + } + + useEffect(() => { + sendFilters() + }, [currentGroup]) + + return ( +
    + +
    + ) +} + +export default JobSkillSearchFilterBar diff --git a/components/Party/index.tsx b/components/Party/index.tsx index 2530de64..677a2ec8 100644 --- a/components/Party/index.tsx +++ b/components/Party/index.tsx @@ -42,9 +42,6 @@ const Party = (props: Props) => { // Set up states const { party } = useSnapshot(appState) - const jobState = party.job - - const [job, setJob] = useState() const [currentTab, setCurrentTab] = useState(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 diff --git a/components/SearchModal/index.tsx b/components/SearchModal/index.tsx index 45d9b5ce..c9ecfaf0 100644 --- a/components/SearchModal/index.tsx +++ b/components/SearchModal/index.tsx @@ -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() 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([]) // 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 ( + { + storeRecentResult(result) + }} + /> + ) + }) + } + + return jsx + } + function openChange() { if (open) { setQuery("") @@ -330,6 +355,11 @@ const SearchModal = (props: Props) => { ) : ( "" )} + {props.object === "job_skills" ? ( + + ) : ( + "" + )}
    diff --git a/components/SummonGrid/index.tsx b/components/SummonGrid/index.tsx index e115da5d..96a3c652 100644 --- a/components/SummonGrid/index.tsx +++ b/components/SummonGrid/index.tsx @@ -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) { diff --git a/components/SummonUnit/index.tsx b/components/SummonUnit/index.tsx index 7128551f..462a9db0 100644 --- a/components/SummonUnit/index.tsx +++ b/components/SummonUnit/index.tsx @@ -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 = ( -
    - {summon?.name.en} - { (props.editable) ? : '' } -
    - ) + function passUncapData(uncap: number) { + if (props.gridSummon) + props.updateUncap(props.gridSummon.id, props.position, uncap) + } - const editableImage = ( - - {image} - - ) + const image = ( +
    + {summon?.name.en} + {props.editable ? ( + + + + ) : ( + "" + )} +
    + ) - const unitContent = ( -
    - { (props.editable) ? editableImage : image } - { (gridSummon) ? - : '' - } -

    {summon?.name[locale]}

    -
    - ) + const editableImage = ( + + {image} + + ) - const withHovercard = ( - - {unitContent} - - ) + const unitContent = ( +
    + {props.editable ? editableImage : image} + {gridSummon ? ( + + ) : ( + "" + )} +

    {summon?.name[locale]}

    +
    + ) - return (gridSummon && !props.editable) ? withHovercard : unitContent + const withHovercard = ( + {unitContent} + ) + + return gridSummon && !props.editable ? withHovercard : unitContent } export default SummonUnit diff --git a/components/UncapIndicator/index.tsx b/components/UncapIndicator/index.tsx index 56de4114..eb292067 100644 --- a/components/UncapIndicator/index.tsx +++ b/components/UncapIndicator/index.tsx @@ -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 = props.uncapLevel : false } key={`star_${i}`} index={i} onClick={toggleStar} /> - } + return numStars + } - const ulb = (i: number) => { - return = props.uncapLevel : false } key={`star_${i}`} index={i} onClick={toggleStar} /> - } - - const flb = (i: number) => { - return = props.uncapLevel : false } key={`star_${i}`} index={i} onClick={toggleStar} /> - } - - const mlb = (i: number) => { - // console.log("MLB; Number of stars:", props.uncapLevel) - return = 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 ( -
      - { - 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) - } - }) - } -
    + = props.uncapLevel : false} + key={`star_${i}`} + index={i} + onClick={toggleStar} + /> ) + } + + const ulb = (i: number) => { + return ( + = props.uncapLevel : false} + key={`star_${i}`} + index={i} + onClick={toggleStar} + /> + ) + } + + const flb = (i: number) => { + return ( + = props.uncapLevel : false} + key={`star_${i}`} + index={i} + onClick={toggleStar} + /> + ) + } + + const mlb = (i: number) => { + // console.log("MLB; Number of stars:", props.uncapLevel) + return ( + = props.uncapLevel : false} + key={`star_${i}`} + index={i} + onClick={toggleStar} + /> + ) + } + + return ( +
      + {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) + } + })} +
    + ) } -export default UncapIndicator \ No newline at end of file +export default UncapIndicator diff --git a/components/WeaponGrid/index.tsx b/components/WeaponGrid/index.tsx index 86169c2c..339a16ed 100644 --- a/components/WeaponGrid/index.tsx +++ b/components/WeaponGrid/index.tsx @@ -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 diff --git a/components/WeaponUnit/index.tsx b/components/WeaponUnit/index.tsx index faecf00f..51edee6e 100644 --- a/components/WeaponUnit/index.tsx +++ b/components/WeaponUnit/index.tsx @@ -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 = ( -
    - {weapon?.name.en} - { (props.editable) ? : '' } -
    + return ( + weapon.ax > 0 || + (weapon.series && [2, 3, 17, 22, 24].includes(weapon.series)) ) + } - const editableImage = ( - - {image} - - ) + const image = ( +
    + {weapon?.name.en} + {props.editable ? ( + + + + ) : ( + "" + )} +
    + ) - const unitContent = ( -
    - { (props.editable && gridWeapon && canBeModified(gridWeapon)) ? - -
    -
    -
    : '' } - { (props.editable) ? editableImage : image } - { (gridWeapon) ? - : '' - } -

    {weapon?.name[locale]}

    -
    - ) + const editableImage = ( + + {image} + + ) - const withHovercard = ( - - {unitContent} - - ) + const unitContent = ( +
    + {props.editable && gridWeapon && canBeModified(gridWeapon) ? ( + +
    +
    +
    + ) : ( + "" + )} + {props.editable ? editableImage : image} + {gridWeapon ? ( + + ) : ( + "" + )} +

    {weapon?.name[locale]}

    +
    + ) - return (gridWeapon && !props.editable) ? withHovercard : unitContent + const withHovercard = ( + {unitContent} + ) + + return gridWeapon && !props.editable ? withHovercard : unitContent } export default WeaponUnit diff --git a/pages/new/index.tsx b/pages/new/index.tsx index b429e8f4..d2762f02 100644 --- a/pages/new/index.tsx +++ b/pages/new/index.tsx @@ -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) => { 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 (
    @@ -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"])), diff --git a/pages/p/[party].tsx b/pages/p/[party].tsx index 9a37db1f..672c8084 100644 --- a/pages/p/[party].tsx +++ b/pages/p/[party].tsx @@ -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) => { + useEffect(() => { + persistStaticData() + }, [persistStaticData]) + + function persistStaticData() { + appState.raids = props.raids + appState.jobs = props.jobs + appState.jobSkills = props.jobSkills + } + return (
    @@ -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"])), diff --git a/public/locales/en/common.json b/public/locales/en/common.json index f43a79a0..63af7fdc 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -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", diff --git a/public/locales/ja/common.json b/public/locales/ja/common.json index 21d147bc..d49f722c 100644 --- a/public/locales/ja/common.json +++ b/public/locales/ja/common.json @@ -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
    Weapons", "coming_soon": "開発中", "no_title": "無題", diff --git a/types/Job.d.ts b/types/Job.d.ts index bab89b90..7c1f5939 100644 --- a/types/Job.d.ts +++ b/types/Job.d.ts @@ -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 - } -} \ No newline at end of file +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 +} diff --git a/types/JobSkill.d.ts b/types/JobSkill.d.ts new file mode 100644 index 00000000..f1067964 --- /dev/null +++ b/types/JobSkill.d.ts @@ -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 +} diff --git a/types/Party.d.ts b/types/Party.d.ts index 5a1dea6b..c43a4d17 100644 --- a/types/Party.d.ts +++ b/types/Party.d.ts @@ -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 diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 00000000..d37ca641 --- /dev/null +++ b/types/index.d.ts @@ -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 +} diff --git a/utils/api.tsx b/utils/api.tsx index 0fd196a7..b8a5987f 100644 --- a/utils/api.tsx +++ b/utils/api.tsx @@ -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 diff --git a/utils/appState.tsx b/utils/appState.tsx index 3a6725a6..42ef90ab 100644 --- a/utils/appState.tsx +++ b/utils/appState.tsx @@ -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) diff --git a/utils/skillGroups.tsx b/utils/skillGroups.tsx new file mode 100644 index 00000000..63d6ccd2 --- /dev/null +++ b/utils/skillGroups.tsx @@ -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: "ベースアビリティ", + }, + }, +]