diff --git a/.gitignore b/.gitignore index 29947561..1ecada63 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ public/images/chara* public/images/job* public/images/awakening* public/images/ax* +public/images/accessory* # Typescript v1 declaration files typings/ diff --git a/components/AboutModal/index.scss b/components/AboutModal/index.scss deleted file mode 100644 index a1c82a2c..00000000 --- a/components/AboutModal/index.scss +++ /dev/null @@ -1,98 +0,0 @@ -.About.DialogContent { - gap: 0; - padding-bottom: $unit; - - .content { - display: flex; - flex-direction: column; - gap: $unit-2x; - padding: 0 $unit-4x; - } - - .sections { - display: flex; - flex-direction: column; - gap: $unit-2x; - } - - section { - margin-bottom: $unit; - - & > h2 { - font-size: $font-medium; - font-weight: $medium; - margin-bottom: $unit * 3; - } - } - - p { - color: var(--text-secondary); - font-size: $font-regular; - line-height: 1.3; - margin-bottom: $unit; - - &:last-of-type { - margin-bottom: 0; - } - } - - .Links { - display: grid; - gap: $unit; - margin: $unit-2x 0; - } - - div.LinkItem { - margin-top: $unit-2x; - } - - .LinkItem { - $diameter: $unit-6x; - border: 1px solid var(--link-item-bg); - border-radius: $card-corner; - - &:hover { - background-color: var(--link-item-bg); - - svg { - fill: var(--link-item-image-color-hover); - } - } - - a { - display: flex; - padding: $unit-2x; - - &:hover { - text-decoration: none; - } - - .Left { - align-items: center; - display: flex; - gap: $unit-2x; - flex-grow: 1; - - h3 { - font-weight: 600; - max-width: 70%; - line-height: 1.3; - } - } - - svg { - fill: var(--link-item-image-color); - width: $diameter; - height: auto; - - &.ShareIcon { - width: $unit-4x; - } - } - } - - h3 { - font-weight: $bold; - } - } -} diff --git a/components/AboutModal/index.tsx b/components/AboutModal/index.tsx deleted file mode 100644 index 7935befd..00000000 --- a/components/AboutModal/index.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import React from 'react' -import Link from 'next/link' -import { useTranslation } from 'next-i18next' - -import { - Dialog, - DialogClose, - DialogTitle, - DialogTrigger, -} from '~components/Dialog' -import DialogContent from '~components/DialogContent' - -import CrossIcon from '~public/icons/Cross.svg' -import ShareIcon from '~public/icons/Share.svg' -import DiscordIcon from '~public/icons/discord.svg' -import GithubIcon from '~public/icons/github.svg' - -import './index.scss' - -const AboutModal = () => { - const { t } = useTranslation('common') - const headerRef = React.createRef() - - return ( - - -
  • - {t('modals.about.title')} -
  • -
    - event.preventDefault()} - onEscapeKeyDown={() => {}} - > -
    - {t('menu.about')} - - - - - -
    - -
    -
    -

    - Granblue.team is a tool to save and share team comps for{' '} - - Granblue Fantasy - - . -

    -

    - Start adding to a team and a URL will be created for you to share - wherever you like, no account needed. -

    -

    - However, if you do make an account, you can save any teams you - find for future reference and keep all of your teams together in - one place. -

    -
    - -
    -

    Feedback

    -

    - This is an evolving project so feedback and suggestions are - greatly appreciated! -

    -

    - If you have a feature request, would like to report a bug, or are - enjoying the tool and want to say thanks, come hang out in - Discord! -

    - -
    - -
    -

    Credits

    -

    - Granblue.team was built by{' '} - - @jedmund - {' '} - with a lot of help from{' '} - - @lalalalinna - {' '} - and{' '} - - @tarngerine - - . -

    -

    - Many thanks also go to Disinfect, Slipper, Jif, Bless, 9highwind, - and everyone else in{' '} - - Fireplace - {' '} - that helped with bug testing and feature requests. (P.S. - We're recruiting!) And yoey, but he won't join our crew. -

    -
    - -
    -

    Contributing

    -

    - This app is open source and licensed under{' '} - - GNU AGPLv3 - - . Plainly, that means you can download the source, modify it, and - redistribute it if you attribute this project, use the same - license, and keep it open source. You can contribute on Github. -

    - -
    -
    -
    -
    - ) -} - -export default AboutModal diff --git a/components/AboutPage/index.scss b/components/AboutPage/index.scss new file mode 100644 index 00000000..38d21a98 --- /dev/null +++ b/components/AboutPage/index.scss @@ -0,0 +1,11 @@ +.About.PageContent { + .Links { + display: grid; + gap: $unit; + margin: $unit-2x 0; + } + + div.LinkItem { + margin-top: $unit-2x; + } +} diff --git a/components/AboutPage/index.tsx b/components/AboutPage/index.tsx new file mode 100644 index 00000000..0e3c546b --- /dev/null +++ b/components/AboutPage/index.tsx @@ -0,0 +1,165 @@ +import React from 'react' +import Link from 'next/link' + +import { useRouter } from 'next/router' +import { useTranslation } from 'next-i18next' + +import ShareIcon from '~public/icons/Share.svg' +import DiscordIcon from '~public/icons/discord.svg' +import GithubIcon from '~public/icons/github.svg' + +import './index.scss' + +interface Props {} + +const AboutPage: React.FC = (props: Props) => { + const { t: common } = useTranslation('common') + return ( +
    +

    {common('about.segmented_control.about')}

    +
    +

    + Granblue.team is a tool to save and share team comps for{' '} + + Granblue Fantasy + + . +

    +

    + Start adding to a team and a URL will be created for you to share + wherever you like, no account needed. +

    +

    + However, if you do make an account, you can save any teams you find + for future reference and keep all of your teams together in one place. +

    +
    + +
    +

    Feedback

    +

    + This is an evolving project so feedback and suggestions are greatly + appreciated! +

    +

    + If you have a feature request, would like to report a bug, or are + enjoying the tool and want to say thanks, come hang out in Discord! +

    +
    + + +
    + +

    granblue-tools

    +
    + +
    + +
    +
    + +
    +

    Credits

    +

    + Granblue.team was built by{' '} + + @jedmund + {' '} + with a lot of help from{' '} + + @lalalalinna + {' '} + and{' '} + + @tarngerine + + . +

    +

    + Many thanks also go to Disinfect, Slipper, Jif, Bless, 9highwind, and + everyone else in{' '} + + Fireplace + {' '} + that helped with bug testing and feature requests. (P.S. We're + recruiting!) And yoey, but he won't join our crew. +

    +
    + +
    +

    Contributing

    +

    + This app is open source and licensed under{' '} + + GNU AGPLv3 + + . Plainly, that means you can download the source, modify it, and + redistribute it if you attribute this project, use the same license, + and keep it open source. You can contribute on Github. +

    + +
    +
    + ) +} + +export default AboutPage diff --git a/components/AccountModal/index.tsx b/components/AccountModal/index.tsx index c10ee25c..72518faa 100644 --- a/components/AccountModal/index.tsx +++ b/components/AccountModal/index.tsx @@ -34,298 +34,312 @@ type StateVariables = { } interface Props { + open: boolean username?: string picture?: string gender?: number language?: string theme?: string private?: boolean + onOpenChange?: (open: boolean) => void } -const AccountModal = (props: Props) => { - // Localization - const { t } = useTranslation('common') - const router = useRouter() - const locale = - router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' +const AccountModal = React.forwardRef( + function AccountModal(props: Props, forwardedRef) { + // Localization + const { t } = useTranslation('common') + const router = useRouter() + const locale = + router.locale && ['en', 'ja'].includes(router.locale) + ? router.locale + : 'en' - // useEffect only runs on the client, so now we can safely show the UI - const [mounted, setMounted] = useState(false) - const { theme: appTheme, setTheme: setAppTheme } = useTheme() + // useEffect only runs on the client, so now we can safely show the UI + const [mounted, setMounted] = useState(false) + const { theme: appTheme, setTheme: setAppTheme } = useTheme() - // Cookies - const accountCookie = getCookie('account') - const userCookie = getCookie('user') + // Cookies + const accountCookie = getCookie('account') + const userCookie = getCookie('user') - const cookieData = { - account: accountCookie ? JSON.parse(accountCookie as string) : undefined, - user: userCookie ? JSON.parse(userCookie as string) : undefined, - } - - // UI State - const [open, setOpen] = useState(false) - const [selectOpenState, setSelectOpenState] = useState({ - picture: false, - gender: false, - language: false, - theme: false, - }) - - // Values - const [username, setUsername] = useState(props.username || '') - const [picture, setPicture] = useState(props.picture || '') - const [language, setLanguage] = useState(props.language || '') - const [gender, setGender] = useState(props.gender || 0) - const [theme, setTheme] = useState(props.theme || 'system') - // const [privateProfile, setPrivateProfile] = useState(false) - - // Setup - const [pictureOpen, setPictureOpen] = useState(false) - const [genderOpen, setGenderOpen] = useState(false) - const [languageOpen, setLanguageOpen] = useState(false) - const [themeOpen, setThemeOpen] = useState(false) - - // Refs - const headerRef = React.createRef() - const footerRef = React.createRef() - - // UI management - function openChange(open: boolean) { - setOpen(open) - } - - function openSelect(name: 'picture' | 'gender' | 'language' | 'theme') { - setPictureOpen(name === 'picture' ? !pictureOpen : false) - setGenderOpen(name === 'gender' ? !genderOpen : false) - setLanguageOpen(name === 'language' ? !languageOpen : false) - setThemeOpen(name === 'theme' ? !themeOpen : false) - } - - // Event handlers - function handlePictureChange(value: string) { - setPicture(value) - } - - function handleLanguageChange(value: string) { - setLanguage(value) - } - - function handleGenderChange(value: string) { - setGender(parseInt(value)) - } - - function handleThemeChange(value: string) { - setTheme(value) - setAppTheme(value) - } - - function onEscapeKeyDown(event: KeyboardEvent) { - if (pictureOpen || genderOpen || languageOpen || themeOpen) { - return event.preventDefault() - } else { - setOpen(false) - } - } - - // API calls - function update(event: React.FormEvent) { - event.preventDefault() - - const object = { - user: { - picture: picture, - element: pictureData.find((i) => i.filename === picture)?.element, - language: language, - gender: gender, - theme: theme, - // private: privateProfile, - }, + const cookieData = { + account: accountCookie ? JSON.parse(accountCookie as string) : undefined, + user: userCookie ? JSON.parse(userCookie as string) : undefined, } - if (accountState.account.user) { - api.endpoints.users - .update(accountState.account.user?.id, object) - .then((response) => { - const user = response.data - - const cookieObj = { - picture: user.avatar.picture, - element: user.avatar.element, - gender: user.gender, - language: user.language, - theme: user.theme, - } - - setCookie('user', cookieObj, { path: '/' }) - - accountState.account.user = { - id: user.id, - username: user.username, - picture: user.avatar.picture, - element: user.avatar.element, - language: user.language, - theme: user.theme, - gender: user.gender, - } - - setOpen(false) - changeLanguage(router, user.language) - }) - } - } - - // Views - const pictureOptions = pictureData - .sort((a, b) => (a.name.en > b.name.en ? 1 : -1)) - .map((item, i) => { - return ( - - {item.name[locale]} - - ) + // UI State + const [open, setOpen] = useState(false) + const [selectOpenState, setSelectOpenState] = useState({ + picture: false, + gender: false, + language: false, + theme: false, }) - const pictureField = () => ( - openSelect('picture')} - onChange={handlePictureChange} - onClose={() => setPictureOpen(false)} - imageAlt={t('modals.settings.labels.image_alt')} - imageClass={pictureData.find((i) => i.filename === picture)?.element} - imageSrc={[`/profile/${picture}.png`, `/profile/${picture}@2x.png 2x`]} - value={picture} - > - {pictureOptions} - - ) + // Values + const [username, setUsername] = useState(props.username || '') + const [picture, setPicture] = useState(props.picture || '') + const [language, setLanguage] = useState(props.language || '') + const [gender, setGender] = useState(props.gender || 0) + const [theme, setTheme] = useState(props.theme || 'system') + // const [privateProfile, setPrivateProfile] = useState(false) - const genderField = () => ( - openSelect('gender')} - onChange={handleGenderChange} - onClose={() => setGenderOpen(false)} - value={`${gender}`} - > - - {t('modals.settings.gender.gran')} - - - {t('modals.settings.gender.djeeta')} - - - ) + // Setup + const [pictureOpen, setPictureOpen] = useState(false) + const [genderOpen, setGenderOpen] = useState(false) + const [languageOpen, setLanguageOpen] = useState(false) + const [themeOpen, setThemeOpen] = useState(false) - const languageField = () => ( - openSelect('language')} - onChange={handleLanguageChange} - onClose={() => setLanguageOpen(false)} - value={language} - > - - {t('modals.settings.language.english')} - - - {t('modals.settings.language.japanese')} - - - ) + // Refs + const headerRef = React.createRef() + const footerRef = React.createRef() - const themeField = () => ( - openSelect('theme')} - onChange={handleThemeChange} - onClose={() => setThemeOpen(false)} - value={theme} - > - - {t('modals.settings.theme.system')} - - - {t('modals.settings.theme.light')} - - - {t('modals.settings.theme.dark')} - - - ) + useEffect(() => { + setOpen(props.open) + }, [props.open]) - useEffect(() => { - setMounted(true) - }, []) + // UI management + function openChange(open: boolean) { + if (props.onOpenChange) props.onOpenChange(open) + setOpen(open) + } - if (!mounted) { - return null - } + function openSelect(name: 'picture' | 'gender' | 'language' | 'theme') { + setPictureOpen(name === 'picture' ? !pictureOpen : false) + setGenderOpen(name === 'gender' ? !genderOpen : false) + setLanguageOpen(name === 'language' ? !languageOpen : false) + setThemeOpen(name === 'theme' ? !themeOpen : false) + } - return ( - - -
  • - {t('menu.settings')} -
  • -
    - {}} - onEscapeKeyDown={onEscapeKeyDown} + // Event handlers + function handlePictureChange(value: string) { + setPicture(value) + } + + function handleLanguageChange(value: string) { + setLanguage(value) + } + + function handleGenderChange(value: string) { + setGender(parseInt(value)) + } + + function handleThemeChange(value: string) { + setTheme(value) + setAppTheme(value) + } + + function onEscapeKeyDown(event: KeyboardEvent) { + if (pictureOpen || genderOpen || languageOpen || themeOpen) { + return event.preventDefault() + } else { + setOpen(false) + } + } + + // API calls + function update(event: React.FormEvent) { + event.preventDefault() + + const object = { + user: { + picture: picture, + element: pictureData.find((i) => i.filename === picture)?.element, + language: language, + gender: gender, + theme: theme, + // private: privateProfile, + }, + } + + if (accountState.account.user) { + api.endpoints.users + .update(accountState.account.user?.id, object) + .then((response) => { + const user = response.data + + const cookieObj = { + avatar: { + picture: user.avatar.picture, + element: user.avatar.element, + }, + gender: user.gender, + language: user.language, + theme: user.theme, + } + + const expiresAt = new Date() + expiresAt.setDate(expiresAt.getDate() + 60) + setCookie('user', cookieObj, { path: '/', expires: expiresAt }) + + accountState.account.user = { + id: user.id, + username: user.username, + granblueId: '', + avatar: { + picture: user.avatar.picture, + element: user.avatar.element, + }, + language: user.language, + theme: user.theme, + gender: user.gender, + } + + setOpen(false) + if (props.onOpenChange) props.onOpenChange(false) + changeLanguage(router, user.language) + }) + } + } + + // Views + const pictureOptions = pictureData + .sort((a, b) => (a.name.en > b.name.en ? 1 : -1)) + .map((item, i) => { + return ( + + {item.name[locale]} + + ) + }) + + const pictureField = () => ( + openSelect('picture')} + onChange={handlePictureChange} + onClose={() => setPictureOpen(false)} + imageAlt={t('modals.settings.labels.image_alt')} + imageClass={pictureData.find((i) => i.filename === picture)?.element} + imageSrc={[`/profile/${picture}.png`, `/profile/${picture}@2x.png 2x`]} + value={picture} > -
    -
    - - {t('modals.settings.title')} - - @{username} -
    - - - - - -
    + {pictureOptions} +
    + ) -
    -
    - {pictureField()} - {genderField()} - {languageField()} - {themeField()} + const genderField = () => ( + openSelect('gender')} + onChange={handleGenderChange} + onClose={() => setGenderOpen(false)} + value={`${gender}`} + > + + {t('modals.settings.gender.gran')} + + + {t('modals.settings.gender.djeeta')} + + + ) + + const languageField = () => ( + openSelect('language')} + onChange={handleLanguageChange} + onClose={() => setLanguageOpen(false)} + value={language} + > + + {t('modals.settings.language.english')} + + + {t('modals.settings.language.japanese')} + + + ) + + const themeField = () => ( + openSelect('theme')} + onChange={handleThemeChange} + onClose={() => setThemeOpen(false)} + value={theme} + > + + {t('modals.settings.theme.system')} + + + {t('modals.settings.theme.light')} + + + {t('modals.settings.theme.dark')} + + + ) + + useEffect(() => { + setMounted(true) + }, []) + + if (!mounted) { + return null + } + + return ( + + {}} + onEscapeKeyDown={onEscapeKeyDown} + > +
    +
    + + {t('modals.settings.title')} + + @{username} +
    + + + + +
    -
    -
    - -
    -
    - ) -} + +
    +
    + {pictureField()} + {genderField()} + {languageField()} + {themeField()} +
    +
    +
    +
    + +
    + ) + } +) export default AccountModal diff --git a/components/Alert/index.tsx b/components/Alert/index.tsx index a9564424..8315495d 100644 --- a/components/Alert/index.tsx +++ b/components/Alert/index.tsx @@ -23,7 +23,11 @@ const Alert = (props: Props) => {
    - {props.title ? Error : ''} + {props.title ? ( + {props.title} + ) : ( + '' + )} {props.message} diff --git a/components/Button/index.scss b/components/Button/index.scss index aedae8c2..994bb041 100644 --- a/components/Button/index.scss +++ b/components/Button/index.scss @@ -31,6 +31,15 @@ background: transparent; } + &.IconButton.medium { + height: inherit; + padding: $unit-half; + + &:hover { + background: none; + } + } + &.Contained { background: var(--button-contained-bg); @@ -43,7 +52,7 @@ stroke: #ff4d4d; } - &.Active.Save { + &.Save { color: #ff4d4d; .Accessory svg { @@ -99,24 +108,27 @@ } } - &.save:hover { - color: #ff4d4d; - + &.Save { .Accessory svg { - fill: #ff4d4d; - stroke: #ff4d4d; + fill: none; + stroke: var(--button-text); } - } - &.save.Active { - color: #ff4d4d; + &.Saved { + color: #ff4d4d; + + .Accessory svg { + fill: #ff4d4d; + stroke: none; + } + } &:hover { - color: darken(#ff4d4d, 30); + color: #ff4d4d; - .icon svg { - fill: darken(#ff4d4d, 30); - stroke: darken(#ff4d4d, 30); + .Accessory svg { + fill: none; + stroke: #ff4d4d; } } } @@ -138,6 +150,10 @@ display: flex; + &.Arrow { + margin-top: $unit-half; + } + svg { fill: var(--button-text); height: $dimension; diff --git a/components/Button/index.tsx b/components/Button/index.tsx index e87e7ff7..8594c129 100644 --- a/components/Button/index.tsx +++ b/components/Button/index.tsx @@ -8,7 +8,10 @@ interface Props React.ButtonHTMLAttributes, HTMLButtonElement > { - accessoryIcon?: React.ReactNode + leftAccessoryIcon?: React.ReactNode + leftAccessoryClassName?: string + rightAccessoryIcon?: React.ReactNode + rightAccessoryClassName?: string active?: boolean blended?: boolean contained?: boolean @@ -24,22 +27,45 @@ const defaultProps = { } const Button = React.forwardRef(function button( - { accessoryIcon, active, blended, contained, buttonSize, text, ...props }, + { + leftAccessoryIcon, + leftAccessoryClassName, + rightAccessoryIcon, + rightAccessoryClassName, + active, + blended, + contained, + buttonSize, + text, + ...props + }, forwardedRef ) { - const classes = classNames( - { - Button: true, - Active: active, - Blended: blended, - Contained: contained, - }, - buttonSize, - props.className - ) + const classes = classNames(buttonSize, props.className, { + Button: true, + Active: active, + Blended: blended, + Contained: contained, + }) - const hasAccessory = () => { - if (accessoryIcon) return {accessoryIcon} + const leftAccessoryClasses = classNames(leftAccessoryClassName, { + Accessory: true, + Left: true, + }) + + const rightAccessoryClasses = classNames(rightAccessoryClassName, { + Accessory: true, + Right: true, + }) + + const hasLeftAccessory = () => { + if (leftAccessoryIcon) + return {leftAccessoryIcon} + } + + const hasRightAccessory = () => { + if (rightAccessoryIcon) + return {rightAccessoryIcon} } const hasText = () => { @@ -48,8 +74,9 @@ const Button = React.forwardRef(function button( return ( ) }) diff --git a/components/ChangelogModal/index.tsx b/components/ChangelogModal/index.tsx deleted file mode 100644 index 5ba0677f..00000000 --- a/components/ChangelogModal/index.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import React from 'react' -import { useTranslation } from 'next-i18next' - -import ChangelogUnit from '~components/ChangelogUnit' -import { - Dialog, - DialogClose, - DialogTitle, - DialogTrigger, -} from '~components/Dialog' -import DialogContent from '~components/DialogContent' - -import CrossIcon from '~public/icons/Cross.svg' - -import './index.scss' - -const ChangelogModal = () => { - const { t } = useTranslation('common') - const headerRef = React.createRef() - - return ( - - -
  • - {t('modals.changelog.title')} -
  • -
    - event.preventDefault()} - onEscapeKeyDown={() => {}} - > -
    - - {t('menu.changelog')} - - - - - - -
    - -
    -
    -
    -

    1.0.1

    - -
    -
      -
    • Extra party fields: Full Auto, Clear Time, and more
    • -
    • Support for Youtube short URLs
    • -
    • Responsive grids and lots of other mobile fixes
    • -
    • Many other bug fixes
    • -
    -
    -
    -
    -

    2022-12 Legend Festival

    - -
    -
    -
    -

    New characters

    -
    - - - -
    -
    -
    -

    New weapons

    -
    - - - -
    -
    -
    -

    New summons

    -
    - -
    -
    -
    -
    -
    -
    -

    2022-12 Flash Gala

    - -
    -
    -
    -

    New characters

    -
    - - -
    -
    -
    -

    New weapons

    -
    - - -
    -
    -
    -
    -
    -
    -

    1.0.0

    - -
    -
      -
    • First release!
    • -
    • You can embed Youtube videos now
    • -
    • Better clicking - right-click and open in a new tab
    • -
    • Manually set dark mode in Account Settings
    • -
    • Lots of bugs squashed
    • -
    -
    -
    -
    -
    - ) -} - -export default ChangelogModal diff --git a/components/CharacterGrid/index.tsx b/components/CharacterGrid/index.tsx index d4c72e6e..944f4e85 100644 --- a/components/CharacterGrid/index.tsx +++ b/components/CharacterGrid/index.tsx @@ -2,8 +2,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react' import { getCookie } from 'cookies-next' import { useSnapshot } from 'valtio' +import { useTranslation } from 'next-i18next' -import { AxiosResponse } from 'axios' +import { AxiosError, AxiosResponse } from 'axios' import debounce from 'lodash.debounce' import Alert from '~components/Alert' @@ -31,12 +32,19 @@ const CharacterGrid = (props: Props) => { // Constants const numCharacters: number = 5 + // Localization + const { t } = useTranslation('common') + // Cookies const cookie = getCookie('account') const accountData: AccountCookie = cookie ? JSON.parse(cookie as string) : null + // Set up state for error handling + const [axiosError, setAxiosError] = useState() + const [errorAlertOpen, setErrorAlertOpen] = useState(false) + // Set up state for view management const { party, grid } = useSnapshot(appState) const [slug, setSlug] = useState() @@ -55,6 +63,7 @@ const CharacterGrid = (props: Props) => { 2: undefined, 3: undefined, }) + const [jobAccessory, setJobAccessory] = useState() const [errorMessage, setErrorMessage] = useState('') // Create a temporary state to store previous weapon uncap values and transcendence stages @@ -81,6 +90,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 @@ -109,7 +119,15 @@ const CharacterGrid = (props: Props) => { if (party.editable) saveCharacter(party.id, character, position) .then((response) => handleCharacterResponse(response.data)) - .catch((error) => console.error(error)) + .catch((error) => { + const axiosError = error as AxiosError + const response = axiosError.response + + if (response) { + setErrorAlertOpen(true) + setAxiosError(response) + } + }) } } @@ -186,7 +204,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 +232,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 +271,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 @@ -462,6 +498,18 @@ const CharacterGrid = (props: Props) => { } // Render: JSX components + const errorAlert = () => { + return ( + setErrorAlertOpen(false)} + cancelActionText={t('buttons.confirm')} + /> + ) + } + return (
    { { })}
    + {errorAlert()}
    ) } diff --git a/components/CharacterUnit/index.tsx b/components/CharacterUnit/index.tsx index d813b877..c9479a95 100644 --- a/components/CharacterUnit/index.tsx +++ b/components/CharacterUnit/index.tsx @@ -164,6 +164,7 @@ const CharacterUnit = ({ function removeCharacter() { if (gridCharacter) sendCharacterToRemove(gridCharacter.id) + setAlertOpen(false) } // Methods: Image string generation @@ -218,7 +219,7 @@ const CharacterUnit = ({