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:
Justin Edmund 2023-06-19 02:35:26 -07:00
parent a671da9e2c
commit 2d368c32cc
5 changed files with 206 additions and 54 deletions

View file

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

View file

@ -56,6 +56,9 @@
.JobSkills {
display: flex;
flex-direction: column;
gap: $unit;
&:not(.editable) {
gap: $unit;
}
}
}

View file

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

View file

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

View file

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