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