Implement job skill search

This commit is contained in:
Justin Edmund 2022-11-30 05:20:22 -08:00
parent ee7dc0bc4a
commit 6ef73583df
9 changed files with 219 additions and 259 deletions

View file

@ -254,7 +254,7 @@ const CharacterGrid = (props: Props) => {
return (
<div>
<div id="CharacterGrid">
<JobSection />
<JobSection editable={party.editable} />
<CharacterConflictModal
open={modalOpen}
incomingCharacter={incoming}

View file

@ -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>

View file

@ -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;

View file

@ -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

View file

@ -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%;
}
}

View file

@ -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

View file

@ -0,0 +1,3 @@
.SearchFilterBar select {
background-color: $grey-90;
}

View 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

View file

@ -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}>