Merge pull request #166 from jedmund/shields-manabelly
Add support for job accessories
This commit is contained in:
commit
c82be5caeb
31 changed files with 728 additions and 77 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -53,6 +53,7 @@ public/images/chara*
|
|||
public/images/job*
|
||||
public/images/awakening*
|
||||
public/images/ax*
|
||||
public/images/accessory*
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ const CharacterGrid = (props: Props) => {
|
|||
2: undefined,
|
||||
3: undefined,
|
||||
})
|
||||
const [jobAccessory, setJobAccessory] = useState<JobAccessory>()
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
|
||||
// Create a temporary state to store previous weapon uncap values and transcendence stages
|
||||
|
|
@ -81,6 +82,7 @@ const CharacterGrid = (props: Props) => {
|
|||
useEffect(() => {
|
||||
setJob(appState.party.job)
|
||||
setJobSkills(appState.party.jobSkills)
|
||||
setJobAccessory(appState.party.accessory)
|
||||
}, [appState])
|
||||
|
||||
// Initialize an array of current uncap values for each characters
|
||||
|
|
@ -186,7 +188,7 @@ const CharacterGrid = (props: Props) => {
|
|||
}
|
||||
|
||||
// Methods: Saving job and job skills
|
||||
const saveJob = async function (job?: Job) {
|
||||
async function saveJob(job?: Job) {
|
||||
const payload = {
|
||||
party: {
|
||||
job_id: job ? job.id : -1,
|
||||
|
|
@ -214,7 +216,7 @@ const CharacterGrid = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
const saveJobSkill = function (skill: JobSkill, position: number) {
|
||||
function saveJobSkill(skill: JobSkill, position: number) {
|
||||
if (party.id && appState.party.editable) {
|
||||
const positionedKey = `skill${position}_id`
|
||||
|
||||
|
|
@ -253,6 +255,24 @@ const CharacterGrid = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
async function saveAccessory(accessory: JobAccessory) {
|
||||
const payload = {
|
||||
party: {
|
||||
accessory_id: accessory.id,
|
||||
},
|
||||
}
|
||||
|
||||
if (appState.party.id) {
|
||||
const response = await api.endpoints.parties.update(
|
||||
appState.party.id,
|
||||
payload
|
||||
)
|
||||
const team = response.data.party
|
||||
setJobAccessory(team.accessory)
|
||||
appState.party.accessory = team.accessory
|
||||
}
|
||||
}
|
||||
|
||||
// Methods: Helpers
|
||||
function characterUncapLevel(character: Character) {
|
||||
let uncapLevel
|
||||
|
|
@ -474,9 +494,11 @@ const CharacterGrid = (props: Props) => {
|
|||
<JobSection
|
||||
job={job}
|
||||
jobSkills={jobSkills}
|
||||
jobAccessory={jobAccessory}
|
||||
editable={party.editable}
|
||||
saveJob={saveJob}
|
||||
saveSkill={saveJobSkill}
|
||||
saveAccessory={saveAccessory}
|
||||
/>
|
||||
<CharacterConflictModal
|
||||
open={modalOpen}
|
||||
|
|
|
|||
52
components/JobAccessoryItem/index.scss
Normal file
52
components/JobAccessoryItem/index.scss
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
.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(--selected-item-bg);
|
||||
|
||||
&:hover {
|
||||
background: var(--selected-item-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
|
||||
67
components/JobAccessoryPopover/index.scss
Normal file
67
components/JobAccessoryPopover/index.scss
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
.JobAccessory.Popover {
|
||||
padding: $unit-2x;
|
||||
min-width: 40vw;
|
||||
max-width: 40vw;
|
||||
max-height: 40vh;
|
||||
overflow-y: scroll;
|
||||
margin-left: $unit-2x;
|
||||
|
||||
h3 {
|
||||
font-size: $font-regular;
|
||||
font-weight: $medium;
|
||||
margin: 0 0 $unit $unit;
|
||||
}
|
||||
|
||||
&.ReadOnly {
|
||||
min-width: inherit;
|
||||
max-width: inherit;
|
||||
}
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
width: initial;
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
@include breakpoint(phone) {
|
||||
width: initial;
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
.Accessories {
|
||||
display: grid;
|
||||
gap: $unit;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.EquippedAccessory {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.Accessory {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
|
||||
h4 {
|
||||
font-size: $font-small;
|
||||
font-weight: $medium;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: $item-corner;
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
155
components/JobAccessoryPopover/index.tsx
Normal file
155
components/JobAccessoryPopover/index.tsx
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
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'
|
||||
import classNames from 'classnames'
|
||||
|
||||
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)
|
||||
|
||||
const classes = classNames({
|
||||
JobAccessory: true,
|
||||
ReadOnly: !editable,
|
||||
})
|
||||
|
||||
// Hooks
|
||||
useEffect(() => {
|
||||
setOpen(modalOpen)
|
||||
}, [modalOpen])
|
||||
|
||||
// Event handlers
|
||||
function handleAccessorySelected(value: string) {
|
||||
onAccessorySelected(value)
|
||||
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)
|
||||
}
|
||||
|
||||
function capitalizeFirstLetter(string: string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
}
|
||||
|
||||
const radioGroup = (
|
||||
<>
|
||||
<h3>
|
||||
{capitalizeFirstLetter(
|
||||
job.accessory_type === 1
|
||||
? `${t('accessories.paladin')}s`
|
||||
: t('accessories.manadiver')
|
||||
)}
|
||||
</h3>
|
||||
<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="EquippedAccessory">
|
||||
<h3>
|
||||
{t('equipped')}{' '}
|
||||
{job.accessory_type === 1
|
||||
? `${t('accessories.paladin')}s`
|
||||
: t('accessories.manadiver')}
|
||||
</h3>
|
||||
<div className="Accessory">
|
||||
<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>
|
||||
</div>
|
||||
) : (
|
||||
<h3>
|
||||
{t('no_accessory', {
|
||||
accessory: t(
|
||||
`accessories.${job.accessory_type === 1 ? 'paladin' : 'manadiver'}`
|
||||
),
|
||||
})}
|
||||
</h3>
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover open={open}>
|
||||
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className={classes}
|
||||
onEscapeKeyDown={closePopover}
|
||||
onPointerDownOutside={handlePointerDownOutside}
|
||||
>
|
||||
{editable ? radioGroup : readOnly}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default JobAccessoryPopover
|
||||
79
components/JobImage/index.scss
Normal file
79
components/JobImage/index.scss
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
.JobImage {
|
||||
$height: 252px;
|
||||
$width: 447px;
|
||||
|
||||
aspect-ratio: 7/9;
|
||||
background: url('/images/background_a.jpg');
|
||||
background-size: 500px 281px;
|
||||
border-radius: $unit;
|
||||
box-sizing: border-box;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
display: block;
|
||||
isolation: isolate;
|
||||
flex-grow: 2;
|
||||
flex-shrink: 0;
|
||||
height: $height;
|
||||
margin-right: $unit * 3;
|
||||
max-height: $height;
|
||||
max-width: $width;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: $width;
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
|
||||
// prettier-ignore
|
||||
@media only screen
|
||||
and (max-width: 800px)
|
||||
and (max-height: 920px)
|
||||
and (-webkit-min-device-pixel-ratio: 2) {
|
||||
margin-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@include breakpoint(phone) {
|
||||
aspect-ratio: 16/9;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
img {
|
||||
-webkit-filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.48));
|
||||
filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.48));
|
||||
position: relative;
|
||||
top: $unit * -4;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.JobAccessory.Button {
|
||||
align-items: center;
|
||||
border-radius: 99px;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: $unit * 1.5;
|
||||
top: $unit;
|
||||
left: $unit;
|
||||
height: auto;
|
||||
z-index: 10;
|
||||
|
||||
&:hover .Accessory svg,
|
||||
&.Selected .Accessory svg {
|
||||
fill: var(--button-text-hover);
|
||||
}
|
||||
|
||||
.Accessory svg {
|
||||
fill: var(--button-text);
|
||||
width: $unit-3x;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.Overlay {
|
||||
background: none;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
114
components/JobImage/index.tsx
Normal file
114
components/JobImage/index.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import React, { useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import Button from '~components/Button'
|
||||
import JobAccessoryPopover from '~components/JobAccessoryPopover'
|
||||
|
||||
import ShieldIcon from '~public/icons/Shield.svg'
|
||||
import ManaturaIcon from '~public/icons/Manatura.svg'
|
||||
|
||||
import './index.scss'
|
||||
import classNames from 'classnames'
|
||||
|
||||
interface Props {
|
||||
job?: Job
|
||||
currentAccessory?: JobAccessory
|
||||
accessories?: JobAccessory[]
|
||||
editable: boolean
|
||||
user?: User
|
||||
onAccessorySelected: (value: string) => void
|
||||
}
|
||||
|
||||
const JobImage = ({
|
||||
job,
|
||||
currentAccessory,
|
||||
editable,
|
||||
accessories,
|
||||
user,
|
||||
onAccessorySelected,
|
||||
}: Props) => {
|
||||
// Localization
|
||||
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 = ''
|
||||
|
||||
if (job) {
|
||||
const slug = job.name.en.replaceAll(' ', '-').toLowerCase()
|
||||
const gender = user && user.gender == 1 ? 'b' : 'a'
|
||||
source = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/jobs/${slug}_${gender}.png`
|
||||
}
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
const hasAccessory = job && job.accessory
|
||||
const image = <img alt={job?.name[locale]} src={imageUrl()} />
|
||||
|
||||
const classes = classNames({
|
||||
JobAccessory: true,
|
||||
Selected: open,
|
||||
})
|
||||
|
||||
function handleAccessoryButtonClicked() {
|
||||
setOpen(!open)
|
||||
}
|
||||
|
||||
function handlePopoverOpenChanged(open: boolean) {
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
// Elements
|
||||
const accessoryButton = () => {
|
||||
let icon
|
||||
|
||||
if (job && job.accessory_type === 1) icon = <ShieldIcon />
|
||||
else if (job && job.accessory_type === 2) icon = <ManaturaIcon />
|
||||
|
||||
return (
|
||||
<Button
|
||||
accessoryIcon={icon}
|
||||
className={classes}
|
||||
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 ? accessoryPopover() : ''}
|
||||
{job && job.id !== '-1' ? image : ''}
|
||||
<div className="Job Overlay" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default JobImage
|
||||
|
|
@ -53,63 +53,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.JobImage {
|
||||
$height: 252px;
|
||||
$width: 447px;
|
||||
|
||||
aspect-ratio: 7/9;
|
||||
background: url('/images/background_a.jpg');
|
||||
background-size: 500px 281px;
|
||||
border-radius: $unit;
|
||||
box-sizing: border-box;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
display: block;
|
||||
isolation: isolate;
|
||||
flex-grow: 2;
|
||||
flex-shrink: 0;
|
||||
height: $height;
|
||||
margin-right: $unit * 3;
|
||||
max-height: $height;
|
||||
max-width: $width;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: $width;
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
|
||||
// prettier-ignore
|
||||
@media only screen
|
||||
and (max-width: 800px)
|
||||
and (max-height: 920px)
|
||||
and (-webkit-min-device-pixel-ratio: 2) {
|
||||
margin-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@include breakpoint(phone) {
|
||||
aspect-ratio: 16/9;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
img {
|
||||
-webkit-filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.48));
|
||||
filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.48));
|
||||
position: relative;
|
||||
top: $unit * -4;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.Overlay {
|
||||
background: none;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.JobSkills {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ import { useSnapshot } from 'valtio'
|
|||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import JobDropdown from '~components/JobDropdown'
|
||||
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'
|
||||
|
|
@ -16,9 +19,11 @@ import './index.scss'
|
|||
interface Props {
|
||||
job?: Job
|
||||
jobSkills: JobSkillObject
|
||||
jobAccessory?: JobAccessory
|
||||
editable: boolean
|
||||
saveJob: (job?: Job) => void
|
||||
saveSkill: (skill: JobSkill, position: number) => void
|
||||
saveAccessory: (accessory: JobAccessory) => void
|
||||
}
|
||||
|
||||
const JobSection = (props: Props) => {
|
||||
|
|
@ -29,13 +34,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(() => {
|
||||
|
|
@ -47,6 +58,7 @@ const JobSection = (props: Props) => {
|
|||
2: props.jobSkills[2],
|
||||
3: props.jobSkills[3],
|
||||
})
|
||||
setCurrentAccessory(props.jobAccessory)
|
||||
|
||||
if (selectRef.current && props.job) selectRef.current.value = props.job.id
|
||||
}, [props])
|
||||
|
|
@ -61,14 +73,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 && job.accessory) {
|
||||
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)
|
||||
props.saveAccessory(accessory)
|
||||
}
|
||||
}
|
||||
|
||||
function generateImageUrl() {
|
||||
let imgSrc = ''
|
||||
|
||||
|
|
@ -130,14 +161,14 @@ const JobSection = (props: Props) => {
|
|||
// Render: JSX components
|
||||
return (
|
||||
<section id="Job">
|
||||
<div className="JobImage">
|
||||
{party.job && party.job.id !== '-1' ? (
|
||||
<img alt={party.job.name[locale]} src={imageUrl} />
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<div className="Job Overlay" />
|
||||
</div>
|
||||
<JobImage
|
||||
job={party.job}
|
||||
currentAccessory={currentAccessory}
|
||||
accessories={accessories}
|
||||
editable={props.editable}
|
||||
user={party.user}
|
||||
onAccessorySelected={handleAccessorySelected}
|
||||
/>
|
||||
<div className="JobDetails">
|
||||
{props.editable ? (
|
||||
<JobDropdown
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ const Party = (props: Props) => {
|
|||
appState.party.updated_at = team.updated_at
|
||||
appState.party.job = team.job
|
||||
appState.party.jobSkills = team.job_skills
|
||||
appState.party.accessory = team.accessory
|
||||
|
||||
appState.party.id = team.id
|
||||
appState.party.extra = team.extra
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
border-radius: $card-corner;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.18);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.24);
|
||||
transform-origin: var(--radix-popover-content-transform-origin);
|
||||
outline: none;
|
||||
padding: $unit;
|
||||
transform-origin: var(--radix-popover-content-transform-origin);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ import './index.scss'
|
|||
|
||||
interface Props
|
||||
extends React.DetailedHTMLProps<
|
||||
React.DialogHTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> {}
|
||||
React.DialogHTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>,
|
||||
PopoverPrimitive.PopoverContentProps {}
|
||||
|
||||
export const Popover = PopoverPrimitive.Root
|
||||
export const PopoverAnchor = PopoverPrimitive.Anchor
|
||||
|
|
@ -26,15 +27,18 @@ export const PopoverContent = React.forwardRef<HTMLDivElement, Props>(
|
|||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
sideOffset={5}
|
||||
{...props}
|
||||
className={classes}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
{children}
|
||||
<PopoverPrimitive.Arrow />
|
||||
<PopoverPrimitive.Arrow className="Arrow" />
|
||||
</PopoverPrimitive.Content>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
PopoverContent.defaultProps = {
|
||||
sideOffset: 8,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@ const SelectItem = React.forwardRef<HTMLDivElement, Props>(function selectItem(
|
|||
{ children, ...props },
|
||||
forwardedRef
|
||||
) {
|
||||
const { altText, iconSrc, ...rest } = props
|
||||
return (
|
||||
<Select.Item
|
||||
className={classNames('SelectItem', props.className)}
|
||||
{...props}
|
||||
{...rest}
|
||||
ref={forwardedRef}
|
||||
value={`${props.value}`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
display: flex;
|
||||
width: $unit-10x;
|
||||
height: $unit-10x;
|
||||
padding: $unit;
|
||||
justify-content: center;
|
||||
z-index: 32;
|
||||
|
||||
|
|
|
|||
81
package-lock.json
generated
81
package-lock.json
generated
|
|
@ -11,6 +11,7 @@
|
|||
"@radix-ui/react-dropdown-menu": "^2.0.1",
|
||||
"@radix-ui/react-hover-card": "^1.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.3",
|
||||
"@radix-ui/react-radio-group": "^1.1.1",
|
||||
"@radix-ui/react-select": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.0.1",
|
||||
"@radix-ui/react-toast": "^1.1.2",
|
||||
|
|
@ -2437,6 +2438,49 @@
|
|||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-radio-group": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.1.tgz",
|
||||
"integrity": "sha512-fmg1CuDKt3GAkL3YnHekmdOicyrXlbp/s/D0MrHa+YB2Un+umpJGheiRowlQtxSpb1eeehKNTINgNESi8WK5rA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-direction": "1.0.0",
|
||||
"@radix-ui/react-presence": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.1",
|
||||
"@radix-ui/react-roving-focus": "1.0.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.0",
|
||||
"@radix-ui/react-use-previous": "1.0.0",
|
||||
"@radix-ui/react-use-size": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz",
|
||||
"integrity": "sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-collection": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-direction": "1.0.0",
|
||||
"@radix-ui/react-id": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.1",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.1.tgz",
|
||||
|
|
@ -8960,6 +9004,43 @@
|
|||
"@radix-ui/react-slot": "1.0.1"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-radio-group": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.1.tgz",
|
||||
"integrity": "sha512-fmg1CuDKt3GAkL3YnHekmdOicyrXlbp/s/D0MrHa+YB2Un+umpJGheiRowlQtxSpb1eeehKNTINgNESi8WK5rA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-direction": "1.0.0",
|
||||
"@radix-ui/react-presence": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.1",
|
||||
"@radix-ui/react-roving-focus": "1.0.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.0",
|
||||
"@radix-ui/react-use-previous": "1.0.0",
|
||||
"@radix-ui/react-use-size": "1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-roving-focus": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz",
|
||||
"integrity": "sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-collection": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-direction": "1.0.0",
|
||||
"@radix-ui/react-id": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.1",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-roving-focus": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"@radix-ui/react-dropdown-menu": "^2.0.1",
|
||||
"@radix-ui/react-hover-card": "^1.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.3",
|
||||
"@radix-ui/react-radio-group": "^1.1.1",
|
||||
"@radix-ui/react-select": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.0.1",
|
||||
"@radix-ui/react-toast": "^1.1.2",
|
||||
|
|
|
|||
3
public/icons/Manatura.svg
Normal file
3
public/icons/Manatura.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.49997 27.5C-0.500051 27.5 1.14279 17.8454 1.78667 15.9435C2.37392 14.2088 3.10735 12.4293 3.96802 10.8994C4.77642 9.46249 5.91051 7.85264 7.45945 6.92577C9.76438 5.54652 12.3986 5.18186 14.3985 4.905C14.8148 4.84736 15.2037 4.79353 15.5563 4.73515C17.7785 4.36726 19.8103 3.78856 21.9629 1.79762C22.9062 0.925157 24.3004 0.750323 25.43 1.36285C26.5595 1.97538 27.1736 3.23925 26.957 4.5058C26.1138 9.43587 23.0247 17.362 14.8822 19.8674C2.17635 23.7769 3.5 27.5 1.49997 27.5Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 612 B |
3
public/icons/Shield.svg
Normal file
3
public/icons/Shield.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.2485 18.9009L23.2738 18.8461L23.2969 18.7903C25.0719 14.5024 25.7609 9.66488 25.9448 6.46207C26.0895 3.94417 24.3044 1.84216 21.9579 1.40085C15.9228 0.265829 11.8484 0.285157 6.02633 1.39844C3.68545 1.84606 1.90969 3.9468 2.05687 6.45935C2.23934 9.57444 2.91185 14.4595 4.70282 18.7895C5.52785 20.7841 6.67748 22.8599 8.06609 24.4951C9.34592 26.0023 11.3727 27.7692 14 27.7692C16.7045 27.7692 18.7211 25.8684 19.9427 24.385C21.2895 22.7497 22.4117 20.7123 23.2485 18.9009Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 610 B |
|
|
@ -6,6 +6,10 @@
|
|||
"roadmap": "Roadmap"
|
||||
}
|
||||
},
|
||||
"accessories": {
|
||||
"paladin": "shield",
|
||||
"manadiver": "manatura"
|
||||
},
|
||||
"alert": {
|
||||
"incompatible_weapon": "You've selected a weapon that can't be added to the Additional Weapon slots."
|
||||
},
|
||||
|
|
@ -378,7 +382,9 @@
|
|||
}
|
||||
},
|
||||
"extra_weapons": "Additional Weapons",
|
||||
"equipped": "Equipped",
|
||||
"coming_soon": "Coming Soon",
|
||||
"no_accessory": "No {{accessory}} equipped",
|
||||
"no_title": "Untitled",
|
||||
"no_raid": "No raid",
|
||||
"no_user": "Anonymous",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@
|
|||
"roadmap": "ロードマップ"
|
||||
}
|
||||
},
|
||||
"accessories": {
|
||||
"paladin": "盾",
|
||||
"manadiver": "マナベリ"
|
||||
},
|
||||
"alert": {
|
||||
"incompatible_weapon": "Additional Weaponsに装備できない武器を入れました。"
|
||||
},
|
||||
|
|
@ -378,8 +382,10 @@
|
|||
"no_skill": "設定されていません"
|
||||
}
|
||||
},
|
||||
"equipped": "装備した",
|
||||
"extra_weapons": "Additional Weapons",
|
||||
"coming_soon": "開発中",
|
||||
"no_accessory": "{{accessory}}は装備していません",
|
||||
"no_title": "無題",
|
||||
"no_raid": "マルチなし",
|
||||
"no_user": "無名",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// use with @include
|
||||
@mixin breakpoint($breakpoint) {
|
||||
$phone-width: 430px;
|
||||
$phone-width: 450px;
|
||||
$phone-height: 920px;
|
||||
|
||||
$tablet-width: 1024px;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@
|
|||
--accent-blue: #{$accent--blue--light};
|
||||
--accent-yellow: #{$accent--yellow--light};
|
||||
|
||||
--selected-item-bg: #{$selected--item--bg--light};
|
||||
--selected-item-bg-hover: #{$selected--item--bg--light--hover};
|
||||
|
||||
// Light - Menus
|
||||
--dialog-bg: #{$dialog--bg--light};
|
||||
|
||||
|
|
@ -143,6 +146,9 @@
|
|||
--accent-blue: #{$accent--blue--dark};
|
||||
--accent-yellow: #{$accent--yellow--dark};
|
||||
|
||||
--selected-item-bg: #{$selected--item--bg--dark};
|
||||
--selected-item-bg-hover: #{$selected--item--bg--dark--hover};
|
||||
|
||||
// Dark - Dialogs
|
||||
--dialog-bg: #{$dialog--bg--dark};
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,11 @@ $accent--blue--dark: #6195f4;
|
|||
$accent--yellow--light: #c89d39;
|
||||
$accent--yellow--dark: #f9cc64;
|
||||
|
||||
$selected--item--bg--dark: #f9cc645d;
|
||||
$selected--item--bg--dark--hover: #fcc33f81;
|
||||
$selected--item--bg--light: #f9cc645d;
|
||||
$selected--item--bg--light--hover: #ecbc4c6f;
|
||||
|
||||
// Colors -- Elements
|
||||
$wind-text-00: #023e28;
|
||||
$wind-text-10: #006a43;
|
||||
|
|
|
|||
2
types/Job.d.ts
vendored
2
types/Job.d.ts
vendored
|
|
@ -14,4 +14,6 @@ interface Job {
|
|||
proficiency2: number
|
||||
}
|
||||
base_job?: Job
|
||||
accessory: boolean
|
||||
accessory_type: number
|
||||
}
|
||||
|
|
|
|||
11
types/JobAccessory.d.ts
vendored
Normal file
11
types/JobAccessory.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
interface JobAccessory {
|
||||
id: string
|
||||
granblue_id: string
|
||||
job: Job
|
||||
name: {
|
||||
[key: string]: string
|
||||
en: string
|
||||
ja: string
|
||||
}
|
||||
rarity: number
|
||||
}
|
||||
1
types/Party.d.ts
vendored
1
types/Party.d.ts
vendored
|
|
@ -20,6 +20,7 @@ interface Party {
|
|||
chain_count?: number
|
||||
job: Job
|
||||
job_skills: JobSkillObject
|
||||
accessory: JobAccessory
|
||||
shortcode: string
|
||||
extra: boolean
|
||||
favorited: boolean
|
||||
|
|
|
|||
|
|
@ -115,6 +115,11 @@ class Api {
|
|||
return axios.get(resourceUrl, params)
|
||||
}
|
||||
|
||||
jobAccessoriesForJob(jobId: string, params?: {}) {
|
||||
const resourceUrl = `${this.url}/jobs/${jobId}/accessories`
|
||||
return axios.get(resourceUrl, params)
|
||||
}
|
||||
|
||||
savedTeams(params: {}) {
|
||||
const resourceUrl = `${this.url}/parties/favorites`
|
||||
return axios.get(resourceUrl, params)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,17 @@ const emptyJob: Job = {
|
|||
},
|
||||
}
|
||||
|
||||
const emptyJobAccessory: JobAccessory = {
|
||||
id: '-1',
|
||||
granblue_id: '-1',
|
||||
job: emptyJob,
|
||||
name: {
|
||||
en: '',
|
||||
ja: '',
|
||||
},
|
||||
rarity: 0,
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
[key: string]: any
|
||||
|
||||
|
|
@ -29,6 +40,7 @@ interface AppState {
|
|||
description: string | undefined
|
||||
job: Job
|
||||
jobSkills: JobSkillObject
|
||||
accessory: JobAccessory
|
||||
raid: Raid | undefined
|
||||
element: number
|
||||
fullAuto: boolean
|
||||
|
|
@ -84,6 +96,7 @@ export const initialAppState: AppState = {
|
|||
2: undefined,
|
||||
3: undefined,
|
||||
},
|
||||
accessory: emptyJobAccessory,
|
||||
raid: undefined,
|
||||
fullAuto: false,
|
||||
autoGuard: false,
|
||||
|
|
|
|||
4
utils/jobsWithAccessories.tsx
Normal file
4
utils/jobsWithAccessories.tsx
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export const ACCESSORY_JOB_IDS = [
|
||||
'683ffee8-4ea2-432d-bc30-4865020ac9f4',
|
||||
'a5d6fca3-5649-4e12-a6db-5fcec49150ee',
|
||||
]
|
||||
|
|
@ -13,7 +13,7 @@ export function printError(error: any, type?: string) {
|
|||
if (type === 'axios') {
|
||||
const response = handleAxiosError(error)
|
||||
console.log(`${response?.status} ${response?.statusText}`)
|
||||
console.log(response?.data.toJSON())
|
||||
console.log(response?.data)
|
||||
} else {
|
||||
console.log(handleError(error))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue