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:
Justin Edmund 2023-01-24 02:07:54 -08:00
parent 075e4d52d7
commit bef9c2b286
6 changed files with 298 additions and 9 deletions

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

View 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

View 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));
}
}

View 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

View file

@ -2,26 +2,42 @@ import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import { ACCESSORY_JOB_IDS } from '~utils/jobsWithAccessories'
import Button from '~components/Button'
import JobAccessoryPopover from '~components/JobAccessoryPopover'
import ShieldIcon from '~public/icons/Shield.svg'
import './index.scss'
interface Props {
job?: Job
currentAccessory?: JobAccessory
accessories?: JobAccessory[]
editable: boolean
user?: User
onAccessoryButtonClicked: () => void
onAccessorySelected: (value: string) => void
}
const ACCESSORY_JOB_IDS = ['683ffee8-4ea2-432d-bc30-4865020ac9f4']
const JobImage = ({ job, user, onAccessoryButtonClicked }: Props) => {
const JobImage = ({
job,
currentAccessory,
editable,
accessories,
user,
onAccessorySelected,
}: 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)
// Refs
const buttonRef = React.createRef<HTMLButtonElement>()
// Static variables
const imageUrl = () => {
let source = ''
@ -38,20 +54,47 @@ const JobImage = ({ job, user, onAccessoryButtonClicked }: Props) => {
const hasAccessory = job && ACCESSORY_JOB_IDS.includes(job.id)
const image = <img alt={job?.name[locale]} src={imageUrl()} />
function handleAccessoryButtonClicked() {
setOpen(!open)
}
function handlePopoverOpenChanged(open: boolean) {
setOpen(open)
}
// Elements
const accessoryButton = () => {
return (
<Button
accessoryIcon={<ShieldIcon />}
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 (
<div className="JobImage">
{hasAccessory ? accessoryButton() : ''}
{hasAccessory ? accessoryPopover() : ''}
{job && job.id !== '-1' ? image : ''}
<div className="Job Overlay" />
</div>

View file

@ -8,7 +8,9 @@ import JobImage from '~components/JobImage'
import JobSkillItem from '~components/JobSkillItem'
import SearchModal from '~components/SearchModal'
import api from '~utils/api'
import { appState } from '~utils/appState'
import { ACCESSORY_JOB_IDS } from '~utils/jobsWithAccessories'
import type { JobSkillObject, SearchableObject } from '~types'
import './index.scss'
@ -30,13 +32,19 @@ const JobSection = (props: Props) => {
const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
// Data state
const [job, setJob] = useState<Job>()
const [imageUrl, setImageUrl] = useState('')
const [numSkills, setNumSkills] = useState(4)
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>()
useEffect(() => {
@ -62,14 +70,33 @@ const JobSection = (props: Props) => {
appState.party.job = job
if (job.row === '1') setNumSkills(3)
else setNumSkills(4)
fetchJobAccessories()
}
}, [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) {
setJob(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() {
let imgSrc = ''
@ -133,8 +160,11 @@ const JobSection = (props: Props) => {
<section id="Job">
<JobImage
job={party.job}
currentAccessory={currentAccessory}
accessories={accessories}
editable={props.editable}
user={party.user}
onAccessoryButtonClicked={() => {}}
onAccessorySelected={handleAccessorySelected}
/>
<div className="JobDetails">
{props.editable ? (