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/awakening*
public/images/ax*
public/images/accessory*
# Typescript v1 declaration files
typings/

View file

@ -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}

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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}`}
>

View file

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

@ -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",

View file

@ -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",

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

View file

@ -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": "無名",

View file

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

View file

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

View file

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

@ -14,4 +14,6 @@ interface Job {
proficiency2: number
}
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
job: Job
job_skills: JobSkillObject
accessory: JobAccessory
shortcode: string
extra: boolean
favorited: boolean

View file

@ -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)

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 {
[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,

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') {
const response = handleAxiosError(error)
console.log(`${response?.status} ${response?.statusText}`)
console.log(response?.data.toJSON())
console.log(response?.data)
} else {
console.log(handleError(error))
}