Implement removing job skills
We added a (...) button next to each editable job skill that opens a context menu that will allow the user to remove the job skill. An alert is presented to make sure the user is sure before proceeding. As part of this change, some minor restyling of JobSkillItem was necessary
This commit is contained in:
parent
a671da9e2c
commit
2d368c32cc
5 changed files with 206 additions and 54 deletions
|
|
@ -259,6 +259,23 @@ const CharacterGrid = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
function removeJobSkill(position: number) {
|
||||
if (party.id && props.editable) {
|
||||
api
|
||||
.removeJobSkill({ partyId: party.id, position: position })
|
||||
.then((response) => {
|
||||
// Update the current skills
|
||||
const newSkills = response.data.job_skills
|
||||
setJobSkills(newSkills)
|
||||
appState.party.jobSkills = newSkills
|
||||
})
|
||||
.catch((error) => {
|
||||
const data = error.response.data
|
||||
console.log(data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAccessory(accessory: JobAccessory) {
|
||||
const payload = {
|
||||
party: {
|
||||
|
|
@ -506,6 +523,7 @@ const CharacterGrid = (props: Props) => {
|
|||
editable={props.editable}
|
||||
saveJob={saveJob}
|
||||
saveSkill={saveJobSkill}
|
||||
removeSkill={removeJobSkill}
|
||||
saveAccessory={saveAccessory}
|
||||
/>
|
||||
<CharacterConflictModal
|
||||
|
|
|
|||
|
|
@ -56,6 +56,9 @@
|
|||
.JobSkills {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
|
||||
&:not(.editable) {
|
||||
gap: $unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'
|
|||
import { useRouter } from 'next/router'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import JobDropdown from '~components/job/JobDropdown'
|
||||
import JobImage from '~components/job/JobImage'
|
||||
|
|
@ -22,6 +23,7 @@ interface Props {
|
|||
editable: boolean
|
||||
saveJob: (job?: Job) => void
|
||||
saveSkill: (skill: JobSkill, position: number) => void
|
||||
removeSkill: (position: number) => void
|
||||
saveAccessory: (accessory: JobAccessory) => void
|
||||
}
|
||||
|
||||
|
|
@ -48,6 +50,12 @@ const JobSection = (props: Props) => {
|
|||
// Refs
|
||||
const selectRef = React.createRef<HTMLSelectElement>()
|
||||
|
||||
// Classes
|
||||
const skillContainerClasses = classNames({
|
||||
JobSkills: true,
|
||||
editable: props.editable,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Set current job based on ID
|
||||
setJob(props.job)
|
||||
|
|
@ -126,9 +134,11 @@ const JobSection = (props: Props) => {
|
|||
return (
|
||||
<JobSkillItem
|
||||
skill={skills[index]}
|
||||
position={index}
|
||||
editable={canEditSkill(skills[index])}
|
||||
key={`skill-${index}`}
|
||||
hasJob={job != undefined && job.id != '-1'}
|
||||
removeJobSkill={props.removeSkill}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -173,10 +183,6 @@ const JobSection = (props: Props) => {
|
|||
</div>
|
||||
)
|
||||
|
||||
function jobLabel() {
|
||||
return job ? filledJobLabel : emptyJobLabel
|
||||
}
|
||||
|
||||
// Render: JSX components
|
||||
return (
|
||||
<section id="Job">
|
||||
|
|
@ -209,7 +215,7 @@ const JobSection = (props: Props) => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<ul className="JobSkills">
|
||||
<ul className={skillContainerClasses}>
|
||||
{[...Array(numSkills)].map((e, i) => (
|
||||
<li key={`job-${i}`}>
|
||||
{canEditSkill(skills[i])
|
||||
|
|
|
|||
|
|
@ -1,47 +1,81 @@
|
|||
.JobSkills {
|
||||
&.editable .JobSkill {
|
||||
.Info {
|
||||
padding: $unit-half * 1.5;
|
||||
|
||||
& > img,
|
||||
& > div.placeholder {
|
||||
width: $unit-4x;
|
||||
height: $unit-4x;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.JobSkill {
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
|
||||
&.editable .Info:hover {
|
||||
background-color: var(--button-bg-hover);
|
||||
}
|
||||
|
||||
&.editable:hover {
|
||||
cursor: pointer;
|
||||
|
||||
& > img.editable,
|
||||
& > div.placeholder.editable {
|
||||
border: $hover-stroke;
|
||||
box-shadow: $hover-shadow;
|
||||
cursor: pointer;
|
||||
transform: $scale-tall;
|
||||
}
|
||||
.Info {
|
||||
& > img.editable,
|
||||
& > div.placeholder.editable {
|
||||
border: $hover-stroke;
|
||||
box-shadow: $hover-shadow;
|
||||
cursor: pointer;
|
||||
transform: $scale-tall;
|
||||
}
|
||||
|
||||
& p.placeholder {
|
||||
color: var(--text-tertiary-hover);
|
||||
}
|
||||
& p.placeholder {
|
||||
color: var(--text-tertiary-hover);
|
||||
}
|
||||
|
||||
& svg {
|
||||
fill: var(--icon-secondary-hover);
|
||||
& svg {
|
||||
fill: var(--icon-secondary-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > img,
|
||||
& > div.placeholder {
|
||||
background: var(--card-bg);
|
||||
border-radius: calc($unit / 2);
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
width: $unit * 5;
|
||||
height: $unit * 5;
|
||||
}
|
||||
|
||||
& > div.placeholder {
|
||||
display: flex;
|
||||
.Info {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: $input-corner;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
gap: $unit;
|
||||
|
||||
& > svg {
|
||||
fill: var(--icon-secondary);
|
||||
width: $unit * 2;
|
||||
height: $unit * 2;
|
||||
& > img,
|
||||
& > div.placeholder {
|
||||
background: var(--card-bg);
|
||||
border-radius: calc($unit / 2);
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
width: $unit-5x;
|
||||
height: $unit-5x;
|
||||
}
|
||||
|
||||
& > div.placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& > svg {
|
||||
fill: var(--icon-secondary);
|
||||
width: $unit-2x;
|
||||
height: $unit-2x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > .Button {
|
||||
justify-content: center;
|
||||
max-width: $unit-6x;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
p {
|
||||
|
|
|
|||
|
|
@ -1,21 +1,43 @@
|
|||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import { Trans, useTranslation } from 'next-i18next'
|
||||
import classNames from 'classnames'
|
||||
import PlusIcon from '~public/icons/Add.svg'
|
||||
|
||||
import Alert from '~components/common/Alert'
|
||||
import Button from '~components/common/Button'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
} from '~components/common/ContextMenu'
|
||||
import ContextMenuItem from '~components/common/ContextMenuItem'
|
||||
|
||||
import EllipsisIcon from '~public/icons/Ellipsis.svg'
|
||||
import PlusIcon from '~public/icons/Add.svg'
|
||||
import './index.scss'
|
||||
|
||||
// Props
|
||||
interface Props extends React.ComponentPropsWithoutRef<'div'> {
|
||||
skill?: JobSkill
|
||||
position: number
|
||||
editable: boolean
|
||||
hasJob: boolean
|
||||
removeJobSkill: (position: number) => void
|
||||
}
|
||||
|
||||
const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
|
||||
function useJobSkillItem({ ...props }, forwardedRef) {
|
||||
function useJobSkillItem(
|
||||
{
|
||||
skill,
|
||||
position,
|
||||
editable,
|
||||
hasJob,
|
||||
removeJobSkill: sendJobSkillToRemove,
|
||||
...props
|
||||
},
|
||||
forwardedRef
|
||||
) {
|
||||
// Set up translation
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation('common')
|
||||
const locale =
|
||||
|
|
@ -23,31 +45,55 @@ const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
|
|||
? router.locale
|
||||
: 'en'
|
||||
|
||||
// States: Component
|
||||
const [alertOpen, setAlertOpen] = useState(false)
|
||||
const [contextMenuOpen, setContextMenuOpen] = useState(false)
|
||||
|
||||
// Classes
|
||||
const classes = classNames({
|
||||
JobSkill: true,
|
||||
editable: props.editable,
|
||||
editable: editable,
|
||||
})
|
||||
|
||||
const imageClasses = classNames({
|
||||
placeholder: !props.skill,
|
||||
editable: props.editable && props.hasJob,
|
||||
placeholder: !skill,
|
||||
editable: editable && hasJob,
|
||||
})
|
||||
|
||||
const buttonClasses = classNames({
|
||||
Clicked: contextMenuOpen,
|
||||
})
|
||||
|
||||
// Methods: Data mutation
|
||||
function removeJobSkill() {
|
||||
if (skill) sendJobSkillToRemove(position)
|
||||
setAlertOpen(false)
|
||||
}
|
||||
|
||||
// Methods: Context menu
|
||||
function handleButtonClicked() {
|
||||
setContextMenuOpen(!contextMenuOpen)
|
||||
}
|
||||
|
||||
function handleContextMenuOpenChange(open: boolean) {
|
||||
if (!open) setContextMenuOpen(false)
|
||||
}
|
||||
|
||||
const skillImage = () => {
|
||||
let jsx: React.ReactNode
|
||||
|
||||
if (props.skill) {
|
||||
if (skill) {
|
||||
jsx = (
|
||||
<img
|
||||
alt={props.skill.name[locale]}
|
||||
alt={skill.name[locale]}
|
||||
className={imageClasses}
|
||||
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-skills/${props.skill.slug}.png`}
|
||||
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-skills/${skill.slug}.png`}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
jsx = (
|
||||
<div className={imageClasses}>
|
||||
{props.editable && props.hasJob ? <PlusIcon /> : ''}
|
||||
{editable && hasJob ? <PlusIcon /> : ''}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -58,9 +104,9 @@ const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
|
|||
const label = () => {
|
||||
let jsx: React.ReactNode
|
||||
|
||||
if (props.skill) {
|
||||
jsx = <p>{props.skill.name[locale]}</p>
|
||||
} else if (props.editable && props.hasJob) {
|
||||
if (skill) {
|
||||
jsx = <p>{skill.name[locale]}</p>
|
||||
} else if (editable && hasJob) {
|
||||
jsx = <p className="placeholder">{t('job_skills.state.selectable')}</p>
|
||||
} else {
|
||||
jsx = <p className="placeholder">{t('job_skills.state.no_skill')}</p>
|
||||
|
|
@ -69,10 +115,55 @@ const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
|
|||
return jsx
|
||||
}
|
||||
|
||||
const removeAlert = () => {
|
||||
return (
|
||||
<Alert
|
||||
open={alertOpen}
|
||||
primaryAction={removeJobSkill}
|
||||
primaryActionText={t('modals.job_skills.buttons.remove')}
|
||||
cancelAction={() => setAlertOpen(false)}
|
||||
cancelActionText={t('buttons.cancel')}
|
||||
message={
|
||||
<Trans i18nKey="modals.job_skills.messages.remove">
|
||||
Are you sure you want to remove{' '}
|
||||
<strong>{{ job_skill: skill?.name[locale] }}</strong> from your
|
||||
team?
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const contextMenu = () => {
|
||||
return (
|
||||
<>
|
||||
<ContextMenu onOpenChange={handleContextMenuOpenChange}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<Button
|
||||
leftAccessoryIcon={<EllipsisIcon />}
|
||||
className={buttonClasses}
|
||||
blended={true}
|
||||
onClick={handleButtonClicked}
|
||||
/>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent align="start">
|
||||
<ContextMenuItem onSelect={() => setAlertOpen(true)}>
|
||||
{t('context.remove_job_skill')}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
{removeAlert()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes} onClick={props.onClick} ref={forwardedRef}>
|
||||
{skillImage()}
|
||||
{label()}
|
||||
<div className={classes} ref={forwardedRef}>
|
||||
<div className="Info" onClick={props.onClick} tabIndex={0}>
|
||||
{skillImage()}
|
||||
{label()}
|
||||
</div>
|
||||
{skill && editable && contextMenu()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue