From 739c72f4e083fe69519e958c2d28d63d6edb600d Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 19 Jun 2023 02:39:27 -0700 Subject: [PATCH] Add ability to remove job skills (#317) * Add Awakening type and remove old defs We remove the flat list of awakening data, as we will be pulling data from the database * Update types to use new Awakening type * Update WeaponUnit for Grand weapon awakenings * Update object modals We needed to update CharacterModal and WeaponModal to display awakenings from the new data format. However, the component used (`SelectWithInput`) was tied to AX Skills in a way that would take exponentially more time to resolve. Instead, we forked `SelectWithInput` into `AwakeningSelectWithInput` and did our work there. `AwakeningSelect` was found to be redundant, so it was removed. * Update hovercards * Add max-height to Select * Allow styling of Select modal with className prop * Add Job class to Job select * Add localizations for removing job skills * Add endpoint for removing job skills * 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 --- components/character/CharacterGrid/index.tsx | 18 +++ components/common/Select/index.scss | 4 +- components/common/Select/index.tsx | 4 +- components/job/JobDropdown/index.tsx | 2 + components/job/JobSection/index.scss | 5 +- components/job/JobSection/index.tsx | 16 ++- components/job/JobSkillItem/index.scss | 94 +++++++++----- components/job/JobSkillItem/index.tsx | 127 ++++++++++++++++--- public/locales/en/common.json | 13 +- public/locales/ja/common.json | 11 +- utils/api.tsx | 5 + 11 files changed, 239 insertions(+), 60 deletions(-) diff --git a/components/character/CharacterGrid/index.tsx b/components/character/CharacterGrid/index.tsx index 958d374e..048ba655 100644 --- a/components/character/CharacterGrid/index.tsx +++ b/components/character/CharacterGrid/index.tsx @@ -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} /> (function Select( )} - + <> (function Select( /> ( onClick={openJobSelect} onOpenChange={() => setOpen(!open)} onValueChange={handleChange} + className="Job" triggerClass="Job" + overlayVisible={false} > {t('no_job')} diff --git a/components/job/JobSection/index.scss b/components/job/JobSection/index.scss index 6caa3093..03782729 100644 --- a/components/job/JobSection/index.scss +++ b/components/job/JobSection/index.scss @@ -56,6 +56,9 @@ .JobSkills { display: flex; flex-direction: column; - gap: $unit; + + &:not(.editable) { + gap: $unit; + } } } diff --git a/components/job/JobSection/index.tsx b/components/job/JobSection/index.tsx index e9e2b1b9..41258d14 100644 --- a/components/job/JobSection/index.tsx +++ b/components/job/JobSection/index.tsx @@ -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() + // 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 ( ) } @@ -173,10 +183,6 @@ const JobSection = (props: Props) => { ) - function jobLabel() { - return job ? filledJobLabel : emptyJobLabel - } - // Render: JSX components return (
@@ -209,7 +215,7 @@ const JobSection = (props: Props) => { )} -
    +
      {[...Array(numSkills)].map((e, i) => (
    • {canEditSkill(skills[i]) diff --git a/components/job/JobSkillItem/index.scss b/components/job/JobSkillItem/index.scss index 9a15bb62..df2fa67b 100644 --- a/components/job/JobSkillItem/index.scss +++ b/components/job/JobSkillItem/index.scss @@ -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 { diff --git a/components/job/JobSkillItem/index.tsx b/components/job/JobSkillItem/index.tsx index bac8f729..a1ded0d3 100644 --- a/components/job/JobSkillItem/index.tsx +++ b/components/job/JobSkillItem/index.tsx @@ -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( - 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( ? 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 = ( {props.skill.name[locale]} ) } else { jsx = (
      - {props.editable && props.hasJob ? : ''} + {editable && hasJob ? : ''}
      ) } @@ -58,9 +104,9 @@ const JobSkillItem = React.forwardRef( const label = () => { let jsx: React.ReactNode - if (props.skill) { - jsx =

      {props.skill.name[locale]}

      - } else if (props.editable && props.hasJob) { + if (skill) { + jsx =

      {skill.name[locale]}

      + } else if (editable && hasJob) { jsx =

      {t('job_skills.state.selectable')}

      } else { jsx =

      {t('job_skills.state.no_skill')}

      @@ -69,10 +115,55 @@ const JobSkillItem = React.forwardRef( return jsx } + const removeAlert = () => { + return ( + setAlertOpen(false)} + cancelActionText={t('buttons.cancel')} + message={ + + Are you sure you want to remove{' '} + {{ job_skill: skill?.name[locale] }} from your + team? + + } + /> + ) + } + + const contextMenu = () => { + return ( + <> + + +