Add JobAccessoryPopover
This opens, closes, and saves the value but it doesn't persist to the server yet. Not a responsive component yet.
This commit is contained in:
parent
075e4d52d7
commit
bef9c2b286
6 changed files with 298 additions and 9 deletions
48
components/JobAccessoryItem/index.scss
Normal file
48
components/JobAccessoryItem/index.scss
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
.JobAccessoryItem {
|
||||||
|
background: none;
|
||||||
|
border-radius: $input-corner;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit;
|
||||||
|
padding: $unit;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&[data-state='checked'] {
|
||||||
|
background: var(--input-bg-hover);
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
color: var(--button-text-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--input-bg-hover);
|
||||||
|
|
||||||
|
img {
|
||||||
|
transform: scale(1.025);
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
color: var(--button-text-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
color: var(--button-text);
|
||||||
|
font-size: $font-small;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: $item-corner;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
transition: $duration-zoom all ease-in-out;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
components/JobAccessoryItem/index.tsx
Normal file
34
components/JobAccessoryItem/index.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
accessory: JobAccessory
|
||||||
|
selected: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const JobAccessoryItem = ({ accessory, selected }: Props) => {
|
||||||
|
// Localization
|
||||||
|
const router = useRouter()
|
||||||
|
const locale =
|
||||||
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadioGroup.Item
|
||||||
|
className="JobAccessoryItem"
|
||||||
|
data-state={selected ? 'checked' : 'unchecked'}
|
||||||
|
value={accessory.id}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt={accessory.name[locale]}
|
||||||
|
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/accessory-grid/${accessory.granblue_id}.jpg`}
|
||||||
|
/>
|
||||||
|
<h4>{accessory.name[locale]}</h4>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JobAccessoryItem
|
||||||
10
components/JobAccessoryPopover/index.scss
Normal file
10
components/JobAccessoryPopover/index.scss
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
.JobAccessory.Popover {
|
||||||
|
padding: $unit-2x;
|
||||||
|
width: 504px;
|
||||||
|
|
||||||
|
.Accessories {
|
||||||
|
display: grid;
|
||||||
|
gap: $unit;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
124
components/JobAccessoryPopover/index.tsx
Normal file
124
components/JobAccessoryPopover/index.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
import React, { PropsWithChildren, useEffect, useState } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { useTranslation } from 'next-i18next'
|
||||||
|
|
||||||
|
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||||
|
|
||||||
|
import Button from '~components/Button'
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
} from '~components/PopoverContent'
|
||||||
|
import JobAccessoryItem from '~components/JobAccessoryItem'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
buttonref: React.RefObject<HTMLButtonElement>
|
||||||
|
currentAccessory?: JobAccessory
|
||||||
|
accessories: JobAccessory[]
|
||||||
|
editable: boolean
|
||||||
|
open: boolean
|
||||||
|
job: Job
|
||||||
|
onAccessorySelected: (value: string) => void
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const JobAccessoryPopover = ({
|
||||||
|
buttonref,
|
||||||
|
currentAccessory,
|
||||||
|
accessories,
|
||||||
|
editable,
|
||||||
|
open: modalOpen,
|
||||||
|
children,
|
||||||
|
job,
|
||||||
|
onAccessorySelected,
|
||||||
|
onOpenChange,
|
||||||
|
}: PropsWithChildren<Props>) => {
|
||||||
|
// Localization
|
||||||
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const locale =
|
||||||
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
|
|
||||||
|
// Component state
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(modalOpen)
|
||||||
|
}, [modalOpen])
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
function handleAccessorySelected() {
|
||||||
|
onAccessorySelected
|
||||||
|
closePopover()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerDownOutside(
|
||||||
|
event: CustomEvent<{ originalEvent: PointerEvent }>
|
||||||
|
) {
|
||||||
|
const target = event.detail.originalEvent.target as Element
|
||||||
|
if (
|
||||||
|
target &&
|
||||||
|
buttonref.current &&
|
||||||
|
target.closest('.JobAccessory.Button') !== buttonref.current
|
||||||
|
) {
|
||||||
|
onOpenChange(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePopover() {
|
||||||
|
onOpenChange(false)
|
||||||
|
}
|
||||||
|
const radioGroup = (
|
||||||
|
<RadioGroup.Root
|
||||||
|
className="Accessories"
|
||||||
|
onValueChange={handleAccessorySelected}
|
||||||
|
>
|
||||||
|
{accessories.map((accessory) => (
|
||||||
|
<JobAccessoryItem
|
||||||
|
accessory={accessory}
|
||||||
|
key={accessory.id}
|
||||||
|
selected={
|
||||||
|
currentAccessory && currentAccessory.id === accessory.id
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</RadioGroup.Root>
|
||||||
|
)
|
||||||
|
|
||||||
|
const readOnly = currentAccessory ? (
|
||||||
|
<div className="JobAccessory">
|
||||||
|
<img
|
||||||
|
alt={currentAccessory.name[locale]}
|
||||||
|
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/accessory-grid/${currentAccessory.granblue_id}.jpg`}
|
||||||
|
/>
|
||||||
|
<h4>{currentAccessory.name[locale]}</h4>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<h3>
|
||||||
|
No shield selected
|
||||||
|
{/* {t('no_accessory', { job: job.name.en.replace(' ', '-').toLowerCase() })} */}
|
||||||
|
</h3>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open}>
|
||||||
|
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="JobAccessory"
|
||||||
|
onEscapeKeyDown={closePopover}
|
||||||
|
onPointerDownOutside={handlePointerDownOutside}
|
||||||
|
>
|
||||||
|
{editable ? radioGroup : readOnly}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JobAccessoryPopover
|
||||||
|
|
@ -2,26 +2,42 @@ import React, { useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
|
|
||||||
|
import { ACCESSORY_JOB_IDS } from '~utils/jobsWithAccessories'
|
||||||
|
|
||||||
import Button from '~components/Button'
|
import Button from '~components/Button'
|
||||||
|
import JobAccessoryPopover from '~components/JobAccessoryPopover'
|
||||||
|
|
||||||
import ShieldIcon from '~public/icons/Shield.svg'
|
import ShieldIcon from '~public/icons/Shield.svg'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
job?: Job
|
job?: Job
|
||||||
|
currentAccessory?: JobAccessory
|
||||||
|
accessories?: JobAccessory[]
|
||||||
|
editable: boolean
|
||||||
user?: User
|
user?: User
|
||||||
onAccessoryButtonClicked: () => void
|
onAccessorySelected: (value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACCESSORY_JOB_IDS = ['683ffee8-4ea2-432d-bc30-4865020ac9f4']
|
const JobImage = ({
|
||||||
|
job,
|
||||||
const JobImage = ({ job, user, onAccessoryButtonClicked }: Props) => {
|
currentAccessory,
|
||||||
|
editable,
|
||||||
|
accessories,
|
||||||
|
user,
|
||||||
|
onAccessorySelected,
|
||||||
|
}: Props) => {
|
||||||
// Localization
|
// Localization
|
||||||
const { t } = useTranslation('common')
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const locale =
|
const locale =
|
||||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
|
|
||||||
|
// Component state
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
// Refs
|
||||||
|
const buttonRef = React.createRef<HTMLButtonElement>()
|
||||||
|
|
||||||
// Static variables
|
// Static variables
|
||||||
const imageUrl = () => {
|
const imageUrl = () => {
|
||||||
let source = ''
|
let source = ''
|
||||||
|
|
@ -38,20 +54,47 @@ const JobImage = ({ job, user, onAccessoryButtonClicked }: Props) => {
|
||||||
const hasAccessory = job && ACCESSORY_JOB_IDS.includes(job.id)
|
const hasAccessory = job && ACCESSORY_JOB_IDS.includes(job.id)
|
||||||
const image = <img alt={job?.name[locale]} src={imageUrl()} />
|
const image = <img alt={job?.name[locale]} src={imageUrl()} />
|
||||||
|
|
||||||
|
function handleAccessoryButtonClicked() {
|
||||||
|
setOpen(!open)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePopoverOpenChanged(open: boolean) {
|
||||||
|
setOpen(open)
|
||||||
|
}
|
||||||
|
|
||||||
// Elements
|
// Elements
|
||||||
const accessoryButton = () => {
|
const accessoryButton = () => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
accessoryIcon={<ShieldIcon />}
|
accessoryIcon={<ShieldIcon />}
|
||||||
className="JobAccessory"
|
className="JobAccessory"
|
||||||
onClick={onAccessoryButtonClicked}
|
onClick={handleAccessoryButtonClicked}
|
||||||
|
ref={buttonRef}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accessoryPopover = () => {
|
||||||
|
return job && accessories ? (
|
||||||
|
<JobAccessoryPopover
|
||||||
|
buttonref={buttonRef}
|
||||||
|
currentAccessory={currentAccessory}
|
||||||
|
accessories={accessories}
|
||||||
|
editable={editable}
|
||||||
|
open={open}
|
||||||
|
job={job}
|
||||||
|
onAccessorySelected={onAccessorySelected}
|
||||||
|
onOpenChange={handlePopoverOpenChanged}
|
||||||
|
>
|
||||||
|
{accessoryButton()}
|
||||||
|
</JobAccessoryPopover>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="JobImage">
|
<div className="JobImage">
|
||||||
{hasAccessory ? accessoryButton() : ''}
|
{hasAccessory ? accessoryPopover() : ''}
|
||||||
{job && job.id !== '-1' ? image : ''}
|
{job && job.id !== '-1' ? image : ''}
|
||||||
<div className="Job Overlay" />
|
<div className="Job Overlay" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@ import JobImage from '~components/JobImage'
|
||||||
import JobSkillItem from '~components/JobSkillItem'
|
import JobSkillItem from '~components/JobSkillItem'
|
||||||
import SearchModal from '~components/SearchModal'
|
import SearchModal from '~components/SearchModal'
|
||||||
|
|
||||||
|
import api from '~utils/api'
|
||||||
import { appState } from '~utils/appState'
|
import { appState } from '~utils/appState'
|
||||||
|
import { ACCESSORY_JOB_IDS } from '~utils/jobsWithAccessories'
|
||||||
import type { JobSkillObject, SearchableObject } from '~types'
|
import type { JobSkillObject, SearchableObject } from '~types'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
@ -30,13 +32,19 @@ const JobSection = (props: Props) => {
|
||||||
const locale =
|
const locale =
|
||||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
|
|
||||||
|
// Data state
|
||||||
const [job, setJob] = useState<Job>()
|
const [job, setJob] = useState<Job>()
|
||||||
const [imageUrl, setImageUrl] = useState('')
|
const [imageUrl, setImageUrl] = useState('')
|
||||||
const [numSkills, setNumSkills] = useState(4)
|
const [numSkills, setNumSkills] = useState(4)
|
||||||
const [skills, setSkills] = useState<{ [key: number]: JobSkill | undefined }>(
|
const [skills, setSkills] = useState<{ [key: number]: JobSkill | undefined }>(
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
const [accessories, setAccessories] = useState<JobAccessory[]>([])
|
||||||
|
const [currentAccessory, setCurrentAccessory] = useState<
|
||||||
|
JobAccessory | undefined
|
||||||
|
>()
|
||||||
|
|
||||||
|
// Refs
|
||||||
const selectRef = React.createRef<HTMLSelectElement>()
|
const selectRef = React.createRef<HTMLSelectElement>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -62,14 +70,33 @@ const JobSection = (props: Props) => {
|
||||||
appState.party.job = job
|
appState.party.job = job
|
||||||
if (job.row === '1') setNumSkills(3)
|
if (job.row === '1') setNumSkills(3)
|
||||||
else setNumSkills(4)
|
else setNumSkills(4)
|
||||||
|
fetchJobAccessories()
|
||||||
}
|
}
|
||||||
}, [job])
|
}, [job])
|
||||||
|
|
||||||
|
// Data fetching
|
||||||
|
async function fetchJobAccessories() {
|
||||||
|
if (job && ACCESSORY_JOB_IDS.includes(job.id)) {
|
||||||
|
const response = await api.jobAccessoriesForJob(job.id)
|
||||||
|
const jobAccessories: JobAccessory[] = response.data
|
||||||
|
setAccessories(jobAccessories)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function receiveJob(job?: Job) {
|
function receiveJob(job?: Job) {
|
||||||
setJob(job)
|
setJob(job)
|
||||||
props.saveJob(job)
|
props.saveJob(job)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAccessorySelected(value: string) {
|
||||||
|
const accessory = accessories.find((accessory) => accessory.id === value)
|
||||||
|
if (accessory) setCurrentAccessory(accessory)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(currentAccessory)
|
||||||
|
}, [currentAccessory])
|
||||||
|
|
||||||
function generateImageUrl() {
|
function generateImageUrl() {
|
||||||
let imgSrc = ''
|
let imgSrc = ''
|
||||||
|
|
||||||
|
|
@ -133,8 +160,11 @@ const JobSection = (props: Props) => {
|
||||||
<section id="Job">
|
<section id="Job">
|
||||||
<JobImage
|
<JobImage
|
||||||
job={party.job}
|
job={party.job}
|
||||||
|
currentAccessory={currentAccessory}
|
||||||
|
accessories={accessories}
|
||||||
|
editable={props.editable}
|
||||||
user={party.user}
|
user={party.user}
|
||||||
onAccessoryButtonClicked={() => {}}
|
onAccessorySelected={handleAccessorySelected}
|
||||||
/>
|
/>
|
||||||
<div className="JobDetails">
|
<div className="JobDetails">
|
||||||
{props.editable ? (
|
{props.editable ? (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue