Implement job skill search
This commit is contained in:
parent
ee7dc0bc4a
commit
6ef73583df
9 changed files with 219 additions and 259 deletions
|
|
@ -254,7 +254,7 @@ const CharacterGrid = (props: Props) => {
|
|||
return (
|
||||
<div>
|
||||
<div id="CharacterGrid">
|
||||
<JobSection />
|
||||
<JobSection editable={party.editable} />
|
||||
<CharacterConflictModal
|
||||
open={modalOpen}
|
||||
incomingCharacter={incoming}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,25 @@
|
|||
import React, { useEffect, useState } from "react"
|
||||
import React, { ForwardedRef, useEffect, useState } from "react"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import { useSnapshot } from "valtio"
|
||||
|
||||
import JobDropdown from "~components/JobDropdown"
|
||||
import JobSkillItem from "~components/JobSkillItem"
|
||||
import SearchModal from "~components/SearchModal"
|
||||
|
||||
import { appState } from "~utils/appState"
|
||||
|
||||
import type { SearchableObject } from "~types"
|
||||
|
||||
import "./index.scss"
|
||||
|
||||
// Props
|
||||
interface Props {}
|
||||
interface Props {
|
||||
editable: boolean
|
||||
}
|
||||
|
||||
const JobSection = (props: Props) => {
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
const [job, setJob] = useState<Job>()
|
||||
const [imageUrl, setImageUrl] = useState("")
|
||||
|
||||
|
|
@ -20,9 +28,11 @@ const JobSection = (props: Props) => {
|
|||
const [numSkills, setNumSkills] = useState(4)
|
||||
const [skills, setSkills] = useState<JobSkill[]>([])
|
||||
|
||||
const [skillRefs, setSkillRefs] = useState<ForwardedRef<HTMLDivElement>[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
// Set current job based on ID
|
||||
setJob(party.job)
|
||||
if (party.job) setJob(party.job)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -33,6 +43,14 @@ const JobSection = (props: Props) => {
|
|||
if (job) appState.party.job = job
|
||||
}, [job])
|
||||
|
||||
useEffect(() => {
|
||||
setSkillRefs(Array(numSkills).fill(React.createRef<HTMLDivElement>()))
|
||||
}, [numSkills])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(skillRefs)
|
||||
}, [skillRefs])
|
||||
|
||||
function receiveJob(job?: Job) {
|
||||
console.log(`Receiving job! Row ${job?.row}: ${job?.name.en}`)
|
||||
if (job) {
|
||||
|
|
@ -62,6 +80,34 @@ const JobSection = (props: Props) => {
|
|||
setImageUrl(imgSrc)
|
||||
}
|
||||
|
||||
const skillItem = (index: number, editable: boolean) => {
|
||||
return (
|
||||
<JobSkillItem
|
||||
skill={skills[index]}
|
||||
editable={!skills[index]?.main && editable}
|
||||
key={`skill-${index}`}
|
||||
hasJob={job != undefined && job.id != "-1"}
|
||||
ref={skillRefs[index]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const editableSkillItem = (index: number) => {
|
||||
return (
|
||||
<SearchModal
|
||||
placeholderText={t("search.placeholders.job_skill")}
|
||||
fromPosition={index}
|
||||
object="job_skills"
|
||||
job={job}
|
||||
send={updateObject}
|
||||
>
|
||||
{skillItem(index, true)}
|
||||
</SearchModal>
|
||||
)
|
||||
}
|
||||
|
||||
function updateObject(object: SearchableObject, position: number) {}
|
||||
|
||||
// Render: JSX components
|
||||
return (
|
||||
<section id="Job">
|
||||
|
|
@ -76,8 +122,10 @@ const JobSection = (props: Props) => {
|
|||
/>
|
||||
<ul className="JobSkills">
|
||||
{[...Array(numSkills)].map((e, i) => (
|
||||
<li>
|
||||
<JobSkillItem skill={skills[i]} editable={!skills[i]?.main} />
|
||||
<li key={`job-${i}`}>
|
||||
{job && job.id != "-1" && !skills[i]?.main && props.editable
|
||||
? editableSkillItem(i)
|
||||
: skillItem(i, false)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
&.editable:hover {
|
||||
cursor: pointer;
|
||||
|
||||
& > img,
|
||||
& > div.placeholder {
|
||||
& > img.editable,
|
||||
& > div.placeholder.editable {
|
||||
border: $hover-stroke;
|
||||
box-shadow: $hover-shadow;
|
||||
cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ import "./index.scss"
|
|||
interface Props {
|
||||
skill?: JobSkill
|
||||
editable: boolean
|
||||
hasJob: boolean
|
||||
}
|
||||
|
||||
const JobSkillItem = (props: Props) => {
|
||||
const JobSkillItem = React.forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation("common")
|
||||
const locale =
|
||||
|
|
@ -25,28 +26,57 @@ const JobSkillItem = (props: Props) => {
|
|||
editable: props.editable,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{props.skill ? (
|
||||
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`}
|
||||
/>
|
||||
) : (
|
||||
<div className="placeholder">
|
||||
<PlusIcon />
|
||||
)
|
||||
} else {
|
||||
jsx = (
|
||||
<div className={imageClasses}>
|
||||
{props.editable && props.hasJob ? <PlusIcon /> : ""}
|
||||
</div>
|
||||
)}
|
||||
<div className="info">
|
||||
{/* {props.skill ? <div className="skill pill">Grouping</div> : ""} */}
|
||||
{props.skill ? (
|
||||
<p>{props.skill.name[locale]}</p>
|
||||
) : (
|
||||
<p className="placeholder">Select a skill</p>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
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">Select a skill</p>
|
||||
} else {
|
||||
jsx = <p className="placeholder">No skill</p>
|
||||
}
|
||||
|
||||
return jsx
|
||||
}
|
||||
|
||||
const skillItem = () => {
|
||||
return (
|
||||
<div className={classes} ref={ref}>
|
||||
{skillImage()}
|
||||
{label()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return skillItem()
|
||||
})
|
||||
|
||||
export default JobSkillItem
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
#Header #Bar select {
|
||||
background-color: $grey-90;
|
||||
}
|
||||
#Header > label {
|
||||
margin: 0 $unit * 3;
|
||||
|
||||
.Input {
|
||||
border: 1px solid $grey-80;
|
||||
border-radius: calc($unit / 1.5);
|
||||
box-sizing: border-box;
|
||||
font-size: $font-regular;
|
||||
padding: $unit * 1.5;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
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 { skillGroups } from "~utils/skillGroups"
|
||||
|
||||
import * as Dialog from "@radix-ui/react-dialog"
|
||||
import JobSkillResult from "~components/JobSkillResult"
|
||||
|
||||
import CrossIcon from "~public/icons/Cross.svg"
|
||||
|
||||
import "./index.scss"
|
||||
|
||||
interface Props {
|
||||
send: (skill: JobSkill, position: number) => any
|
||||
job?: Job
|
||||
fromPosition: number
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const JobSkillModal = (props: Props) => {
|
||||
// Set up router
|
||||
const router = useRouter()
|
||||
const locale = router.locale
|
||||
|
||||
// Set up translation
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
let searchInput = React.createRef<HTMLInputElement>()
|
||||
let scrollContainer = React.createRef<HTMLDivElement>()
|
||||
|
||||
const [currentGroup, setCurrentGroup] = useState(-1)
|
||||
const [currentGroupName, setCurrentGroupName] = useState("")
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState("")
|
||||
const [results, setResults] = useState<JobSkill[]>([])
|
||||
|
||||
// Pagination states
|
||||
const [recordCount, setRecordCount] = useState(0)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
|
||||
useEffect(() => {
|
||||
setResults(appState.jobSkills.filter((skill) => skill.main === false))
|
||||
setRecordCount(
|
||||
appState.jobSkills.filter((skill) => skill.main === false).length
|
||||
)
|
||||
}, [appState, setResults])
|
||||
|
||||
useEffect(() => {
|
||||
if (searchInput.current) searchInput.current.focus()
|
||||
}, [searchInput])
|
||||
|
||||
useEffect(() => {
|
||||
setRecordCount(results.length)
|
||||
}, [results])
|
||||
|
||||
useEffect(() => {
|
||||
const name = skillGroups
|
||||
.find((skill) => skill.id === currentGroup)
|
||||
?.name["en"].toLowerCase()
|
||||
setCurrentGroupName(name ? name : "")
|
||||
}, [currentGroup])
|
||||
|
||||
function onChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const newValue = parseInt(event.target.value)
|
||||
setCurrentGroup(newValue)
|
||||
|
||||
if (newValue >= 0) {
|
||||
setResults(
|
||||
appState.jobSkills.filter((skill, i) => {
|
||||
if (newValue === 4) {
|
||||
return skill.emp && !skill.main
|
||||
} else if (newValue === 5) {
|
||||
return skill.base && !skill.main
|
||||
} else {
|
||||
return skill.color === newValue && !skill.main
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
setResults(appState.jobSkills.filter((skill) => skill.main === false))
|
||||
}
|
||||
}
|
||||
|
||||
function inputChanged(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const text = event.target.value
|
||||
if (text.length) {
|
||||
setQuery(text)
|
||||
} else {
|
||||
setQuery("")
|
||||
}
|
||||
}
|
||||
|
||||
function openChange() {
|
||||
if (open) {
|
||||
setQuery("")
|
||||
// setFirstLoad(true)
|
||||
setResults([])
|
||||
setRecordCount(0)
|
||||
setCurrentPage(1)
|
||||
setOpen(false)
|
||||
} else {
|
||||
setOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
function onBlur() {}
|
||||
|
||||
function render() {
|
||||
const rows = results.map((result: JobSkill, i: number) => {
|
||||
return (
|
||||
<JobSkillResult data={result} key={`skill-${i}`} onClick={() => {}} />
|
||||
)
|
||||
})
|
||||
return (
|
||||
<InfiniteScroll
|
||||
dataLength={results && results.length > 0 ? results.length : 0}
|
||||
next={() => setCurrentPage(currentPage + 1)}
|
||||
hasMore={totalPages > currentPage}
|
||||
scrollableTarget="Results"
|
||||
loader={<div className="footer">Loading...</div>}
|
||||
>
|
||||
{rows}
|
||||
</InfiniteScroll>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>{props.children}</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content className="Search Dialog">
|
||||
<div id="Header">
|
||||
<div id="Bar">
|
||||
<select
|
||||
key="job-skill-groups"
|
||||
value={currentGroup}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
>
|
||||
<option key="all" value={-1}>
|
||||
All skills
|
||||
</option>
|
||||
<option key="damaging" value={2}>
|
||||
Damaging
|
||||
</option>
|
||||
<option key="buffing" value={0}>
|
||||
Buffing
|
||||
</option>
|
||||
<option key="debuffing" value={1}>
|
||||
Debuffing
|
||||
</option>
|
||||
<option key="healing" value={3}>
|
||||
Healing
|
||||
</option>
|
||||
<option key="emp" value={4}>
|
||||
Extended Mastery
|
||||
</option>
|
||||
<option key="base" value={5}>
|
||||
Base Skills
|
||||
</option>
|
||||
</select>
|
||||
<Dialog.Close className="DialogClose" onClick={openChange}>
|
||||
<CrossIcon />
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
<label className="search_label" htmlFor="search_input">
|
||||
<input
|
||||
autoComplete="off"
|
||||
type="text"
|
||||
name="query"
|
||||
className="Input"
|
||||
id="search_input"
|
||||
ref={searchInput}
|
||||
value={query}
|
||||
placeholder={
|
||||
currentGroupName
|
||||
? `Search for ${currentGroupName} skills...`
|
||||
: `Search all skills...`
|
||||
}
|
||||
onChange={inputChanged}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="Results" ref={scrollContainer}>
|
||||
<h5 className="total">
|
||||
{t("search.result_count", { record_count: recordCount })}
|
||||
</h5>
|
||||
{open ? render() : ""}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default JobSkillModal
|
||||
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
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
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"
|
||||
|
|
@ -13,10 +13,13 @@ 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"
|
||||
|
|
@ -27,14 +30,12 @@ interface Props {
|
|||
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
|
||||
|
|
@ -46,10 +47,7 @@ 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<SearchableObjectArray>([])
|
||||
|
|
@ -59,10 +57,6 @@ const SearchModal = (props: Props) => {
|
|||
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])
|
||||
|
|
@ -77,15 +71,19 @@ const SearchModal = (props: Props) => {
|
|||
}
|
||||
|
||||
function fetchResults({ replace = false }: { replace?: boolean }) {
|
||||
console.log("Fetch results!!!")
|
||||
api
|
||||
.search({
|
||||
object: props.object,
|
||||
query: query,
|
||||
job: props.job?.id,
|
||||
filters: filters,
|
||||
locale: locale,
|
||||
page: currentPage,
|
||||
})
|
||||
.then((response) => {
|
||||
console.log("resp")
|
||||
console.log(response)
|
||||
setTotalPages(response.data.total_pages)
|
||||
setRecordCount(response.data.count)
|
||||
|
||||
|
|
@ -152,7 +150,7 @@ const SearchModal = (props: Props) => {
|
|||
openChange()
|
||||
}
|
||||
|
||||
function receiveFilters(filters: { [key: string]: number[] }) {
|
||||
function receiveFilters(filters: { [key: string]: any }) {
|
||||
setCurrentPage(1)
|
||||
setResults([])
|
||||
setFilters(filters)
|
||||
|
|
@ -208,6 +206,9 @@ const SearchModal = (props: Props) => {
|
|||
case "characters":
|
||||
jsx = renderCharacterSearchResults(results)
|
||||
break
|
||||
case "job_skills":
|
||||
jsx = renderJobSkillSearchResults(results)
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -286,6 +287,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("")
|
||||
|
|
@ -338,6 +360,11 @@ const SearchModal = (props: Props) => {
|
|||
) : (
|
||||
""
|
||||
)}
|
||||
{props.object === "job_skills" ? (
|
||||
<JobSkillSearchFilterBar sendFilters={receiveFilters} />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div id="Results" ref={scrollContainer}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue