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 { 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>
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
Loading…
Reference in a new issue