diff --git a/components/AccountModal/index.tsx b/components/AccountModal/index.tsx index 0567380e..3cea1cca 100644 --- a/components/AccountModal/index.tsx +++ b/components/AccountModal/index.tsx @@ -34,300 +34,306 @@ 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, - } - - const expiresAt = new Date() - expiresAt.setDate(expiresAt.getDate() + 60) - setCookie('user', cookieObj, { path: '/', expires: expiresAt }) - - 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 = { + 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, + 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]} + + ) + }) + + 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..585fb876 100644 --- a/components/Button/index.scss +++ b/components/Button/index.scss @@ -43,7 +43,7 @@ stroke: #ff4d4d; } - &.Active.Save { + &.Save { color: #ff4d4d; .Accessory svg { @@ -99,24 +99,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 +141,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/CharacterGrid/index.tsx b/components/CharacterGrid/index.tsx index f0b59039..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() @@ -111,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) + } + }) } } @@ -482,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..6e1b0674 100644 --- a/components/CharacterUnit/index.tsx +++ b/components/CharacterUnit/index.tsx @@ -218,7 +218,7 @@ const CharacterUnit = ({