Merge pull request #166 from jedmund/shields-manabelly

Add support for job accessories
This commit is contained in:
Justin Edmund 2023-01-27 11:41:05 -08:00 committed by GitHub
commit c82be5caeb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 728 additions and 77 deletions

1
.gitignore vendored
View file

@ -53,6 +53,7 @@ public/images/chara*
public/images/job* public/images/job*
public/images/awakening* public/images/awakening*
public/images/ax* public/images/ax*
public/images/accessory*
# Typescript v1 declaration files # Typescript v1 declaration files
typings/ typings/

View file

@ -55,6 +55,7 @@ const CharacterGrid = (props: Props) => {
2: undefined, 2: undefined,
3: undefined, 3: undefined,
}) })
const [jobAccessory, setJobAccessory] = useState<JobAccessory>()
const [errorMessage, setErrorMessage] = useState('') const [errorMessage, setErrorMessage] = useState('')
// Create a temporary state to store previous weapon uncap values and transcendence stages // Create a temporary state to store previous weapon uncap values and transcendence stages
@ -81,6 +82,7 @@ const CharacterGrid = (props: Props) => {
useEffect(() => { useEffect(() => {
setJob(appState.party.job) setJob(appState.party.job)
setJobSkills(appState.party.jobSkills) setJobSkills(appState.party.jobSkills)
setJobAccessory(appState.party.accessory)
}, [appState]) }, [appState])
// Initialize an array of current uncap values for each characters // Initialize an array of current uncap values for each characters
@ -186,7 +188,7 @@ const CharacterGrid = (props: Props) => {
} }
// Methods: Saving job and job skills // Methods: Saving job and job skills
const saveJob = async function (job?: Job) { async function saveJob(job?: Job) {
const payload = { const payload = {
party: { party: {
job_id: job ? job.id : -1, 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) { if (party.id && appState.party.editable) {
const positionedKey = `skill${position}_id` 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 // Methods: Helpers
function characterUncapLevel(character: Character) { function characterUncapLevel(character: Character) {
let uncapLevel let uncapLevel
@ -474,9 +494,11 @@ const CharacterGrid = (props: Props) => {
<JobSection <JobSection
job={job} job={job}
jobSkills={jobSkills} jobSkills={jobSkills}
jobAccessory={jobAccessory}
editable={party.editable} editable={party.editable}
saveJob={saveJob} saveJob={saveJob}
saveSkill={saveJobSkill} saveSkill={saveJobSkill}
saveAccessory={saveAccessory}
/> />
<CharacterConflictModal <CharacterConflictModal
open={modalOpen} open={modalOpen}

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

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

View 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

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

View 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

View file

@ -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 { .JobSkills {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -4,10 +4,13 @@ import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import JobDropdown from '~components/JobDropdown' import JobDropdown from '~components/JobDropdown'
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'
@ -16,9 +19,11 @@ import './index.scss'
interface Props { interface Props {
job?: Job job?: Job
jobSkills: JobSkillObject jobSkills: JobSkillObject
jobAccessory?: JobAccessory
editable: boolean editable: boolean
saveJob: (job?: Job) => void saveJob: (job?: Job) => void
saveSkill: (skill: JobSkill, position: number) => void saveSkill: (skill: JobSkill, position: number) => void
saveAccessory: (accessory: JobAccessory) => void
} }
const JobSection = (props: Props) => { const JobSection = (props: Props) => {
@ -29,13 +34,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(() => {
@ -47,6 +58,7 @@ const JobSection = (props: Props) => {
2: props.jobSkills[2], 2: props.jobSkills[2],
3: props.jobSkills[3], 3: props.jobSkills[3],
}) })
setCurrentAccessory(props.jobAccessory)
if (selectRef.current && props.job) selectRef.current.value = props.job.id if (selectRef.current && props.job) selectRef.current.value = props.job.id
}, [props]) }, [props])
@ -61,14 +73,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 && job.accessory) {
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)
props.saveAccessory(accessory)
}
}
function generateImageUrl() { function generateImageUrl() {
let imgSrc = '' let imgSrc = ''
@ -130,14 +161,14 @@ const JobSection = (props: Props) => {
// Render: JSX components // Render: JSX components
return ( return (
<section id="Job"> <section id="Job">
<div className="JobImage"> <JobImage
{party.job && party.job.id !== '-1' ? ( job={party.job}
<img alt={party.job.name[locale]} src={imageUrl} /> currentAccessory={currentAccessory}
) : ( accessories={accessories}
'' editable={props.editable}
)} user={party.user}
<div className="Job Overlay" /> onAccessorySelected={handleAccessorySelected}
</div> />
<div className="JobDetails"> <div className="JobDetails">
{props.editable ? ( {props.editable ? (
<JobDropdown <JobDropdown

View file

@ -147,6 +147,7 @@ const Party = (props: Props) => {
appState.party.updated_at = team.updated_at appState.party.updated_at = team.updated_at
appState.party.job = team.job appState.party.job = team.job
appState.party.jobSkills = team.job_skills appState.party.jobSkills = team.job_skills
appState.party.accessory = team.accessory
appState.party.id = team.id appState.party.id = team.id
appState.party.extra = team.extra appState.party.extra = team.extra

View file

@ -4,6 +4,7 @@
border-radius: $card-corner; border-radius: $card-corner;
border: 0.5px solid rgba(0, 0, 0, 0.18); border: 0.5px solid rgba(0, 0, 0, 0.18);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.24); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.24);
transform-origin: var(--radix-popover-content-transform-origin);
outline: none; outline: none;
padding: $unit;
transform-origin: var(--radix-popover-content-transform-origin);
} }

View file

@ -6,9 +6,10 @@ import './index.scss'
interface Props interface Props
extends React.DetailedHTMLProps< extends React.DetailedHTMLProps<
React.DialogHTMLAttributes<HTMLDivElement>, React.DialogHTMLAttributes<HTMLDivElement>,
HTMLDivElement HTMLDivElement
> {} >,
PopoverPrimitive.PopoverContentProps {}
export const Popover = PopoverPrimitive.Root export const Popover = PopoverPrimitive.Root
export const PopoverAnchor = PopoverPrimitive.Anchor export const PopoverAnchor = PopoverPrimitive.Anchor
@ -26,15 +27,18 @@ export const PopoverContent = React.forwardRef<HTMLDivElement, Props>(
return ( return (
<PopoverPrimitive.Portal> <PopoverPrimitive.Portal>
<PopoverPrimitive.Content <PopoverPrimitive.Content
sideOffset={5}
{...props} {...props}
className={classes} className={classes}
ref={forwardedRef} ref={forwardedRef}
> >
{children} {children}
<PopoverPrimitive.Arrow /> <PopoverPrimitive.Arrow className="Arrow" />
</PopoverPrimitive.Content> </PopoverPrimitive.Content>
</PopoverPrimitive.Portal> </PopoverPrimitive.Portal>
) )
} }
) )
PopoverContent.defaultProps = {
sideOffset: 8,
}

View file

@ -14,10 +14,11 @@ const SelectItem = React.forwardRef<HTMLDivElement, Props>(function selectItem(
{ children, ...props }, { children, ...props },
forwardedRef forwardedRef
) { ) {
const { altText, iconSrc, ...rest } = props
return ( return (
<Select.Item <Select.Item
className={classNames('SelectItem', props.className)} className={classNames('SelectItem', props.className)}
{...props} {...rest}
ref={forwardedRef} ref={forwardedRef}
value={`${props.value}`} value={`${props.value}`}
> >

View file

@ -5,7 +5,6 @@
display: flex; display: flex;
width: $unit-10x; width: $unit-10x;
height: $unit-10x; height: $unit-10x;
padding: $unit;
justify-content: center; justify-content: center;
z-index: 32; z-index: 32;

81
package-lock.json generated
View file

@ -11,6 +11,7 @@
"@radix-ui/react-dropdown-menu": "^2.0.1", "@radix-ui/react-dropdown-menu": "^2.0.1",
"@radix-ui/react-hover-card": "^1.0.2", "@radix-ui/react-hover-card": "^1.0.2",
"@radix-ui/react-popover": "^1.0.3", "@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-select": "^1.1.2",
"@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-switch": "^1.0.1",
"@radix-ui/react-toast": "^1.1.2", "@radix-ui/react-toast": "^1.1.2",
@ -2437,6 +2438,49 @@
"react-dom": "^16.8 || ^17.0 || ^18.0" "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": { "node_modules/@radix-ui/react-roving-focus": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.1.tgz", "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-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": { "@radix-ui/react-roving-focus": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.1.tgz",

View file

@ -16,6 +16,7 @@
"@radix-ui/react-dropdown-menu": "^2.0.1", "@radix-ui/react-dropdown-menu": "^2.0.1",
"@radix-ui/react-hover-card": "^1.0.2", "@radix-ui/react-hover-card": "^1.0.2",
"@radix-ui/react-popover": "^1.0.3", "@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-select": "^1.1.2",
"@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-switch": "^1.0.1",
"@radix-ui/react-toast": "^1.1.2", "@radix-ui/react-toast": "^1.1.2",

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

View file

@ -6,6 +6,10 @@
"roadmap": "Roadmap" "roadmap": "Roadmap"
} }
}, },
"accessories": {
"paladin": "shield",
"manadiver": "manatura"
},
"alert": { "alert": {
"incompatible_weapon": "You've selected a weapon that can't be added to the Additional Weapon slots." "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", "extra_weapons": "Additional Weapons",
"equipped": "Equipped",
"coming_soon": "Coming Soon", "coming_soon": "Coming Soon",
"no_accessory": "No {{accessory}} equipped",
"no_title": "Untitled", "no_title": "Untitled",
"no_raid": "No raid", "no_raid": "No raid",
"no_user": "Anonymous", "no_user": "Anonymous",

View file

@ -6,6 +6,10 @@
"roadmap": "ロードマップ" "roadmap": "ロードマップ"
} }
}, },
"accessories": {
"paladin": "盾",
"manadiver": "マナベリ"
},
"alert": { "alert": {
"incompatible_weapon": "Additional Weaponsに装備できない武器を入れました。" "incompatible_weapon": "Additional Weaponsに装備できない武器を入れました。"
}, },
@ -378,8 +382,10 @@
"no_skill": "設定されていません" "no_skill": "設定されていません"
} }
}, },
"equipped": "装備した",
"extra_weapons": "Additional Weapons", "extra_weapons": "Additional Weapons",
"coming_soon": "開発中", "coming_soon": "開発中",
"no_accessory": "{{accessory}}は装備していません",
"no_title": "無題", "no_title": "無題",
"no_raid": "マルチなし", "no_raid": "マルチなし",
"no_user": "無名", "no_user": "無名",

View file

@ -1,6 +1,6 @@
// use with @include // use with @include
@mixin breakpoint($breakpoint) { @mixin breakpoint($breakpoint) {
$phone-width: 430px; $phone-width: 450px;
$phone-height: 920px; $phone-height: 920px;
$tablet-width: 1024px; $tablet-width: 1024px;

View file

@ -16,6 +16,9 @@
--accent-blue: #{$accent--blue--light}; --accent-blue: #{$accent--blue--light};
--accent-yellow: #{$accent--yellow--light}; --accent-yellow: #{$accent--yellow--light};
--selected-item-bg: #{$selected--item--bg--light};
--selected-item-bg-hover: #{$selected--item--bg--light--hover};
// Light - Menus // Light - Menus
--dialog-bg: #{$dialog--bg--light}; --dialog-bg: #{$dialog--bg--light};
@ -143,6 +146,9 @@
--accent-blue: #{$accent--blue--dark}; --accent-blue: #{$accent--blue--dark};
--accent-yellow: #{$accent--yellow--dark}; --accent-yellow: #{$accent--yellow--dark};
--selected-item-bg: #{$selected--item--bg--dark};
--selected-item-bg-hover: #{$selected--item--bg--dark--hover};
// Dark - Dialogs // Dark - Dialogs
--dialog-bg: #{$dialog--bg--dark}; --dialog-bg: #{$dialog--bg--dark};

View file

@ -83,6 +83,11 @@ $accent--blue--dark: #6195f4;
$accent--yellow--light: #c89d39; $accent--yellow--light: #c89d39;
$accent--yellow--dark: #f9cc64; $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 // Colors -- Elements
$wind-text-00: #023e28; $wind-text-00: #023e28;
$wind-text-10: #006a43; $wind-text-10: #006a43;

2
types/Job.d.ts vendored
View file

@ -14,4 +14,6 @@ interface Job {
proficiency2: number proficiency2: number
} }
base_job?: Job base_job?: Job
accessory: boolean
accessory_type: number
} }

11
types/JobAccessory.d.ts vendored Normal file
View 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
View file

@ -20,6 +20,7 @@ interface Party {
chain_count?: number chain_count?: number
job: Job job: Job
job_skills: JobSkillObject job_skills: JobSkillObject
accessory: JobAccessory
shortcode: string shortcode: string
extra: boolean extra: boolean
favorited: boolean favorited: boolean

View file

@ -115,6 +115,11 @@ class Api {
return axios.get(resourceUrl, params) return axios.get(resourceUrl, params)
} }
jobAccessoriesForJob(jobId: string, params?: {}) {
const resourceUrl = `${this.url}/jobs/${jobId}/accessories`
return axios.get(resourceUrl, params)
}
savedTeams(params: {}) { savedTeams(params: {}) {
const resourceUrl = `${this.url}/parties/favorites` const resourceUrl = `${this.url}/parties/favorites`
return axios.get(resourceUrl, params) return axios.get(resourceUrl, params)

View file

@ -18,6 +18,17 @@ const emptyJob: Job = {
}, },
} }
const emptyJobAccessory: JobAccessory = {
id: '-1',
granblue_id: '-1',
job: emptyJob,
name: {
en: '',
ja: '',
},
rarity: 0,
}
interface AppState { interface AppState {
[key: string]: any [key: string]: any
@ -29,6 +40,7 @@ interface AppState {
description: string | undefined description: string | undefined
job: Job job: Job
jobSkills: JobSkillObject jobSkills: JobSkillObject
accessory: JobAccessory
raid: Raid | undefined raid: Raid | undefined
element: number element: number
fullAuto: boolean fullAuto: boolean
@ -84,6 +96,7 @@ export const initialAppState: AppState = {
2: undefined, 2: undefined,
3: undefined, 3: undefined,
}, },
accessory: emptyJobAccessory,
raid: undefined, raid: undefined,
fullAuto: false, fullAuto: false,
autoGuard: false, autoGuard: false,

View file

@ -0,0 +1,4 @@
export const ACCESSORY_JOB_IDS = [
'683ffee8-4ea2-432d-bc30-4865020ac9f4',
'a5d6fca3-5649-4e12-a6db-5fcec49150ee',
]

View file

@ -13,7 +13,7 @@ export function printError(error: any, type?: string) {
if (type === 'axios') { if (type === 'axios') {
const response = handleAxiosError(error) const response = handleAxiosError(error)
console.log(`${response?.status} ${response?.statusText}`) console.log(`${response?.status} ${response?.statusText}`)
console.log(response?.data.toJSON()) console.log(response?.data)
} else { } else {
console.log(handleError(error)) console.log(handleError(error))
} }