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:
parent
c599a8352a
commit
79a0095d22
5 changed files with 291 additions and 144 deletions
|
|
@ -1,25 +1,25 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
import api from '~utils/api'
|
||||
import { appState } from '~utils/appState'
|
||||
import { jobGroups } from '~utils/jobGroups'
|
||||
import { appState } from "~utils/appState"
|
||||
import { jobGroups } from "~utils/jobGroups"
|
||||
|
||||
import './index.scss'
|
||||
import "./index.scss"
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
currentJob?: string
|
||||
onChange?: (job?: Job) => void
|
||||
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
||||
currentJob?: string
|
||||
onChange?: (job?: Job) => void
|
||||
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
||||
}
|
||||
|
||||
type GroupedJob = { [key: string]: Job[] }
|
||||
|
||||
const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFieldSet(props, ref) {
|
||||
const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
|
||||
function useFieldSet(props, ref) {
|
||||
// Set up router for locale
|
||||
const router = useRouter()
|
||||
const locale = router.locale || 'en'
|
||||
const locale = router.locale || "en"
|
||||
|
||||
// Set up local states for storing jobs
|
||||
const [currentJob, setCurrentJob] = useState<Job>()
|
||||
|
|
@ -27,71 +27,78 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(function useField
|
|||
const [sortedJobs, setSortedJobs] = useState<GroupedJob>()
|
||||
|
||||
// Organize jobs into groups on mount
|
||||
const organizeJobs = useCallback((jobs: Job[]) => {
|
||||
const jobGroups = jobs.map(job => job.row).filter((value, index, self) => self.indexOf(value) === index)
|
||||
let groupedJobs: GroupedJob = {}
|
||||
|
||||
jobGroups.forEach(group => {
|
||||
groupedJobs[group] = jobs.filter(job => job.row === group)
|
||||
})
|
||||
|
||||
setJobs(jobs)
|
||||
setSortedJobs(groupedJobs)
|
||||
appState.jobs = jobs
|
||||
}, [])
|
||||
|
||||
// Fetch all jobs on mount
|
||||
useEffect(() => {
|
||||
api.endpoints.jobs.getAll()
|
||||
.then(response => organizeJobs(response.data))
|
||||
}, [organizeJobs])
|
||||
const jobGroups = appState.jobs
|
||||
.map((job) => job.row)
|
||||
.filter((value, index, self) => self.indexOf(value) === index)
|
||||
let groupedJobs: GroupedJob = {}
|
||||
|
||||
jobGroups.forEach((group) => {
|
||||
groupedJobs[group] = appState.jobs.filter((job) => job.row === group)
|
||||
})
|
||||
|
||||
setJobs(appState.jobs)
|
||||
setSortedJobs(groupedJobs)
|
||||
}, [appState])
|
||||
|
||||
// Set current job on mount
|
||||
useEffect(() => {
|
||||
if (jobs && props.currentJob) {
|
||||
const job = jobs.find(job => job.id === props.currentJob)
|
||||
setCurrentJob(job)
|
||||
}
|
||||
}, [jobs, props.currentJob])
|
||||
if (jobs && props.currentJob) {
|
||||
const job = appState.jobs.find((job) => job.id === props.currentJob)
|
||||
setCurrentJob(job)
|
||||
}
|
||||
}, [appState, props.currentJob])
|
||||
|
||||
// Enable changing select value
|
||||
function handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
if (jobs) {
|
||||
const job = jobs.find(job => job.id === event.target.value)
|
||||
if (props.onChange) props.onChange(job)
|
||||
setCurrentJob(job)
|
||||
}
|
||||
if (jobs) {
|
||||
const job = jobs.find((job) => job.id === event.target.value)
|
||||
if (props.onChange) props.onChange(job)
|
||||
setCurrentJob(job)
|
||||
}
|
||||
}
|
||||
|
||||
// Render JSX for each job option, sorted into optgroups
|
||||
function renderJobGroup(group: string) {
|
||||
const options = sortedJobs && sortedJobs[group].length > 0 &&
|
||||
sortedJobs[group].sort((a, b) => a.order - b.order).map((item, i) => {
|
||||
return (
|
||||
<option key={i} value={item.id}>{item.name[locale]}</option>
|
||||
)
|
||||
})
|
||||
const options =
|
||||
sortedJobs &&
|
||||
sortedJobs[group].length > 0 &&
|
||||
sortedJobs[group]
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map((item, i) => {
|
||||
return (
|
||||
<option key={i} value={item.id}>
|
||||
{item.name[locale]}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
|
||||
const groupName = jobGroups.find(g => g.slug === group)?.name[locale]
|
||||
const groupName = jobGroups.find((g) => g.slug === group)?.name[locale]
|
||||
|
||||
return (
|
||||
<optgroup key={group} label={groupName}>
|
||||
{options}
|
||||
</optgroup>
|
||||
)
|
||||
return (
|
||||
<optgroup key={group} label={groupName}>
|
||||
{options}
|
||||
</optgroup>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<select
|
||||
key={currentJob?.id}
|
||||
value={currentJob?.id}
|
||||
onBlur={props.onBlur}
|
||||
onChange={handleChange}
|
||||
ref={ref}>
|
||||
<option key="no-job" value={-1}>No class</option>
|
||||
{ (sortedJobs) ? Object.keys(sortedJobs).map(x => renderJobGroup(x)) : '' }
|
||||
</select>
|
||||
<select
|
||||
key={currentJob?.id}
|
||||
value={currentJob?.id}
|
||||
onBlur={props.onBlur}
|
||||
onChange={handleChange}
|
||||
ref={ref}
|
||||
>
|
||||
<option key="no-job" value={-1}>
|
||||
No class
|
||||
</option>
|
||||
{sortedJobs
|
||||
? Object.keys(sortedJobs).map((x) => renderJobGroup(x))
|
||||
: ""}
|
||||
</select>
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default JobDropdown
|
||||
|
|
|
|||
|
|
@ -1,44 +1,65 @@
|
|||
#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%;
|
||||
|
||||
select {
|
||||
flex-grow: 1;
|
||||
width: auto;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.JobImage {
|
||||
$height: 249px;
|
||||
$width: 447px;
|
||||
|
||||
background: url('/images/background_a.jpg');
|
||||
background-size: 500px 281px;
|
||||
border-radius: $unit;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
display: block;
|
||||
flex-grow: 2;
|
||||
height: $height;
|
||||
margin-right: $unit * 3;
|
||||
max-height: $height;
|
||||
max-width: $width;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: $width;
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
top: $unit * -4;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.Overlay {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
.JobSkills {
|
||||
flex-grow: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.JobImage {
|
||||
$height: 249px;
|
||||
$width: 447px;
|
||||
|
||||
background: url("/images/background_a.jpg");
|
||||
background-size: 500px 281px;
|
||||
border-radius: $unit;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
display: block;
|
||||
flex-grow: 2;
|
||||
flex-shrink: 0;
|
||||
height: $height;
|
||||
margin-right: $unit * 3;
|
||||
max-height: $height;
|
||||
max-width: $width;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: $width;
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
|
||||
img {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +1,89 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import React, { useEffect, useState } from "react"
|
||||
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
|
||||
interface Props {}
|
||||
|
||||
const JobSection = (props: Props) => {
|
||||
const [job, setJob] = useState<Job>()
|
||||
const [imageUrl, setImageUrl] = useState('')
|
||||
const [job, setJob] = useState<Job>()
|
||||
const [imageUrl, setImageUrl] = useState("")
|
||||
|
||||
const { party } = useSnapshot(appState)
|
||||
const { party } = useSnapshot(appState)
|
||||
|
||||
useEffect(() => {
|
||||
// Set current job based on ID
|
||||
setJob(party.job)
|
||||
}, [])
|
||||
const [numSkills, setNumSkills] = useState(4)
|
||||
const [skills, setSkills] = useState<JobSkill[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
generateImageUrl()
|
||||
})
|
||||
useEffect(() => {
|
||||
// Set current job based on ID
|
||||
setJob(party.job)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (job) appState.party.job = job
|
||||
}, [job])
|
||||
useEffect(() => {
|
||||
generateImageUrl()
|
||||
})
|
||||
|
||||
function receiveJob(job?: Job) {
|
||||
setJob(job)
|
||||
useEffect(() => {
|
||||
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() {
|
||||
let imgSrc = ""
|
||||
|
||||
if (job) {
|
||||
const slug = job?.name.en.replaceAll(' ', '-').toLowerCase()
|
||||
const gender = (party.user && party.user.gender == 1) ? 'b' : 'a'
|
||||
setImageUrl(imgSrc)
|
||||
}
|
||||
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/jobs/${slug}_${gender}.png`
|
||||
}
|
||||
|
||||
setImageUrl(imgSrc)
|
||||
}
|
||||
|
||||
// Render: JSX components
|
||||
return (
|
||||
<section id="Job">
|
||||
<div className="JobImage">
|
||||
<img src={imageUrl} />
|
||||
<div className="Overlay" />
|
||||
</div>
|
||||
<JobDropdown
|
||||
currentJob={ (party.job) ? party.job.id : undefined}
|
||||
onChange={receiveJob}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
// Render: JSX components
|
||||
return (
|
||||
<section id="Job">
|
||||
<div className="JobImage">
|
||||
<img src={imageUrl} />
|
||||
<div className="Overlay" />
|
||||
</div>
|
||||
<div className="JobDetails">
|
||||
<JobDropdown
|
||||
currentJob={party.job ? party.job.id : undefined}
|
||||
onChange={receiveJob}
|
||||
/>
|
||||
<ul className="JobSkills">
|
||||
{[...Array(numSkills)].map((e, i) => (
|
||||
<li>
|
||||
<JobSkillItem skill={skills[i]} editable={!skills[i]?.main} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default JobSection
|
||||
export default JobSection
|
||||
|
|
|
|||
42
components/JobSkillItem/index.scss
Normal file
42
components/JobSkillItem/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
52
components/JobSkillItem/index.tsx
Normal file
52
components/JobSkillItem/index.tsx
Normal 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
|
||||
Loading…
Reference in a new issue