Add basic interface for skills

Skills change when the job changes, but can't be selected on their own yet
This commit is contained in:
Justin Edmund 2022-11-28 20:36:12 -08:00
parent c599a8352a
commit 79a0095d22
5 changed files with 291 additions and 144 deletions

View file

@ -1,25 +1,25 @@
import React, { useCallback, useEffect, useState } from 'react' import React, { useEffect, useState } from "react"
import { useRouter } from 'next/router' import { useRouter } from "next/router"
import api from '~utils/api' import { appState } from "~utils/appState"
import { appState } from '~utils/appState' import { jobGroups } from "~utils/jobGroups"
import { jobGroups } from '~utils/jobGroups'
import './index.scss' import "./index.scss"
// Props // Props
interface Props { interface Props {
currentJob?: string currentJob?: string
onChange?: (job?: Job) => void onChange?: (job?: Job) => void
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
} }
type GroupedJob = { [key: string]: Job[] } type GroupedJob = { [key: string]: Job[] }
const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFieldSet(props, ref) { const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
function useFieldSet(props, ref) {
// Set up router for locale // Set up router for locale
const router = useRouter() const router = useRouter()
const locale = router.locale || 'en' const locale = router.locale || "en"
// Set up local states for storing jobs // Set up local states for storing jobs
const [currentJob, setCurrentJob] = useState<Job>() const [currentJob, setCurrentJob] = useState<Job>()
@ -27,71 +27,78 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(function useField
const [sortedJobs, setSortedJobs] = useState<GroupedJob>() const [sortedJobs, setSortedJobs] = useState<GroupedJob>()
// Organize jobs into groups on mount // 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
}, [])
// Fetch all jobs on mount
useEffect(() => { useEffect(() => {
api.endpoints.jobs.getAll() const jobGroups = appState.jobs
.then(response => organizeJobs(response.data)) .map((job) => job.row)
}, [organizeJobs]) .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 // Set current job on mount
useEffect(() => { useEffect(() => {
if (jobs && props.currentJob) { if (jobs && props.currentJob) {
const job = jobs.find(job => job.id === props.currentJob) const job = appState.jobs.find((job) => job.id === props.currentJob)
setCurrentJob(job) setCurrentJob(job)
} }
}, [jobs, props.currentJob]) }, [appState, props.currentJob])
// Enable changing select value // Enable changing select value
function handleChange(event: React.ChangeEvent<HTMLSelectElement>) { function handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
if (jobs) { if (jobs) {
const job = jobs.find(job => job.id === event.target.value) const job = jobs.find((job) => job.id === event.target.value)
if (props.onChange) props.onChange(job) if (props.onChange) props.onChange(job)
setCurrentJob(job) setCurrentJob(job)
} }
} }
// Render JSX for each job option, sorted into optgroups // Render JSX for each job option, sorted into optgroups
function renderJobGroup(group: string) { function renderJobGroup(group: string) {
const options = sortedJobs && sortedJobs[group].length > 0 && const options =
sortedJobs[group].sort((a, b) => a.order - b.order).map((item, i) => { sortedJobs &&
return ( sortedJobs[group].length > 0 &&
<option key={i} value={item.id}>{item.name[locale]}</option> sortedJobs[group]
) .sort((a, b) => a.order - b.order)
}) .map((item, i) => {
return (
<option key={i} value={item.id}>
{item.name[locale]}
</option>
)
})
const groupName = jobGroups.find(g => g.slug === group)?.name[locale] const groupName = jobGroups.find((g) => g.slug === group)?.name[locale]
return ( return (
<optgroup key={group} label={groupName}> <optgroup key={group} label={groupName}>
{options} {options}
</optgroup> </optgroup>
) )
} }
return ( return (
<select <select
key={currentJob?.id} key={currentJob?.id}
value={currentJob?.id} value={currentJob?.id}
onBlur={props.onBlur} onBlur={props.onBlur}
onChange={handleChange} onChange={handleChange}
ref={ref}> ref={ref}
<option key="no-job" value={-1}>No class</option> >
{ (sortedJobs) ? Object.keys(sortedJobs).map(x => renderJobGroup(x)) : '' } <option key="no-job" value={-1}>
</select> No class
</option>
{sortedJobs
? Object.keys(sortedJobs).map((x) => renderJobGroup(x))
: ""}
</select>
) )
}) }
)
export default JobDropdown export default JobDropdown

View file

@ -1,44 +1,65 @@
#Job { #Job {
display: flex;
margin-bottom: $unit * 3;
select {
flex-grow: 1;
width: auto;
}
.JobDetails {
display: flex; display: flex;
margin-bottom: $unit * 3; flex-direction: column;
width: 100%;
select { select {
flex-grow: 1; flex-grow: 0;
width: auto;
} }
.JobImage { .JobSkills {
$height: 249px; flex-grow: 2;
$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;
}
} }
} }
.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 {
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 {
display: flex;
flex-direction: column;
gap: $unit;
}
}

View file

@ -1,64 +1,89 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from "react"
import { useSnapshot } from 'valtio' import { useSnapshot } from "valtio"
import JobDropdown from '~components/JobDropdown' import JobDropdown from "~components/JobDropdown"
import JobSkillItem from "~components/JobSkillItem"
import { appState } from '~utils/appState' import { appState } from "~utils/appState"
import './index.scss' import "./index.scss"
// Props // Props
interface Props {} interface Props {}
const JobSection = (props: Props) => { const JobSection = (props: Props) => {
const [job, setJob] = useState<Job>() const [job, setJob] = useState<Job>()
const [imageUrl, setImageUrl] = useState('') const [imageUrl, setImageUrl] = useState("")
const { party } = useSnapshot(appState) const { party } = useSnapshot(appState)
useEffect(() => { const [numSkills, setNumSkills] = useState(4)
// Set current job based on ID const [skills, setSkills] = useState<JobSkill[]>([])
setJob(party.job)
}, [])
useEffect(() => { useEffect(() => {
generateImageUrl() // Set current job based on ID
}) setJob(party.job)
}, [])
useEffect(() => { useEffect(() => {
if (job) appState.party.job = job generateImageUrl()
}, [job]) })
function receiveJob(job?: Job) { useEffect(() => {
setJob(job) if (job) appState.party.job = job
}, [job])
function receiveJob(job?: Job) {
console.log(`Receiving job! Row ${job?.row}: ${job?.name.en}`)
if (job) {
setJob(job)
const baseSkills = appState.jobSkills.filter(
(skill) => skill.job.id === job.id && skill.main
)
if (job.row === "1") setNumSkills(3)
else setNumSkills(4)
setSkills(baseSkills)
}
}
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`
} }
function generateImageUrl() { setImageUrl(imgSrc)
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` // Render: JSX components
} return (
<section id="Job">
setImageUrl(imgSrc) <div className="JobImage">
} <img src={imageUrl} />
<div className="Overlay" />
// Render: JSX components </div>
return ( <div className="JobDetails">
<section id="Job"> <JobDropdown
<div className="JobImage"> currentJob={party.job ? party.job.id : undefined}
<img src={imageUrl} /> onChange={receiveJob}
<div className="Overlay" /> />
</div> <ul className="JobSkills">
<JobDropdown {[...Array(numSkills)].map((e, i) => (
currentJob={ (party.job) ? party.job.id : undefined} <li>
onChange={receiveJob} <JobSkillItem skill={skills[i]} editable={!skills[i]?.main} />
/> </li>
</section> ))}
) </ul>
</div>
</section>
)
} }
export default JobSection export default JobSection

View file

@ -0,0 +1,42 @@
.JobSkill {
display: flex;
gap: $unit;
align-items: center;
&:hover p.placeholder {
color: $grey-20;
}
&.editable:hover > img,
&.editable:hover > .placeholder {
border: $hover-stroke;
box-shadow: $hover-shadow;
cursor: pointer;
transform: $scale-tall;
}
& > img,
& > .placeholder {
background: white;
border-radius: calc($unit / 2);
border: 1px solid rgba(0, 0, 0, 0);
width: $unit * 5;
height: $unit * 5;
}
& > .placeholder {
display: flex;
align-items: center;
justify-content: center;
& > svg {
fill: $grey-60;
width: $unit * 2;
height: $unit * 2;
}
}
p.placeholder {
color: $grey-50;
}
}

View file

@ -0,0 +1,52 @@
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 {
skill?: JobSkill
editable: boolean
}
const JobSkillItem = (props: Props) => {
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,
})
return (
<div className={classes}>
{props.skill ? (
<img
alt={props.skill.name[locale]}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}job-skills/${props.skill.slug}.png`}
/>
) : (
<div className="placeholder">
<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>
)}
</div>
</div>
)
}
export default JobSkillItem