diff --git a/components/AccountModal/index.scss b/components/AccountModal/index.scss index 4c3f258b..781fb148 100644 --- a/components/AccountModal/index.scss +++ b/components/AccountModal/index.scss @@ -2,7 +2,7 @@ display: flex; flex-direction: column; gap: $unit * 2; - width: $unit * 60; + width: $unit * 64; form { display: flex; @@ -45,89 +45,6 @@ transform: translateX(21px); } } - - .field { - align-items: center; - display: flex; - flex-direction: row; - gap: $unit * 2; - - select { - background: no-repeat url('/icons/ArrowDark.svg'), $grey-90; - background-position-y: center; - background-position-x: 95%; - margin: 0; - width: 240px; - } - - .left { - display: flex; - flex-direction: column; - flex-grow: 1; - gap: calc($unit / 2); - - label { - color: var(--text-secondary); - font-size: $font-regular; - } - - p { - color: var(--text-secondary); - font-size: $font-small; - line-height: 1.1; - max-width: 300px; - - &.jp { - max-width: 270px; - } - } - } - - .preview { - $diameter: 48px; - background-color: $grey-90; - border-radius: 999px; - height: $diameter; - width: $diameter; - - img { - height: $diameter; - width: $diameter; - } - - &.fire { - background: $fire-bg-20; - } - - &.water { - background: $water-bg-20; - } - - &.wind { - background: $wind-bg-20; - } - - &.earth { - background: $earth-bg-20; - } - - &.dark { - background: $dark-bg-10; - } - - &.light { - background: $light-bg-20; - } - } - } - - section { - margin-bottom: $unit; - - h2 { - margin-bottom: $unit * 3; - } - } } .DialogDescription { diff --git a/components/AccountModal/index.tsx b/components/AccountModal/index.tsx index ab8438c4..d3e1b615 100644 --- a/components/AccountModal/index.tsx +++ b/components/AccountModal/index.tsx @@ -1,74 +1,123 @@ import React, { useEffect, useState } from 'react' -import { getCookie } from 'cookies-next' +import { getCookie, setCookie } from 'cookies-next' import { useRouter } from 'next/router' -import { useSnapshot } from 'valtio' import { useTranslation } from 'next-i18next' -import * as Dialog from '@radix-ui/react-dialog' -import * as Switch from '@radix-ui/react-switch' +import { + Dialog, + DialogClose, + DialogContent, + DialogTitle, + DialogTrigger, +} from '~components/Dialog' +import Button from '~components/Button' +import SelectItem from '~components/SelectItem' +import PictureSelectItem from '~components/PictureSelectItem' +import SelectTableField from '~components/SelectTableField' +// import * as Switch from '@radix-ui/react-switch' import api from '~utils/api' +import changeLanguage from 'utils/changeLanguage' import { accountState } from '~utils/accountState' import { pictureData } from '~utils/pictureData' -import Button from '~components/Button' - import CrossIcon from '~public/icons/Cross.svg' import './index.scss' +import { useTheme } from 'next-themes' -const AccountModal = () => { - const { account } = useSnapshot(accountState) +type StateVariables = { + [key: string]: boolean + picture: boolean + gender: boolean + language: boolean + theme: boolean +} - const router = useRouter() +interface Props { + username?: string + picture?: string + gender?: number + language?: string + theme?: string + private?: boolean +} + +const AccountModal = (props: Props) => { + // Localization const { t } = useTranslation('common') + const router = useRouter() const locale = router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' - // State + // 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') + + 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 [picture, setPicture] = useState('') - const [language, setLanguage] = useState('') - const [gender, setGender] = useState(0) - const [privateProfile, setPrivateProfile] = useState(false) + const [selectOpenState, setSelectOpenState] = useState({ + picture: false, + gender: false, + language: false, + theme: false, + }) - // Refs - const pictureSelect = React.createRef() - const languageSelect = React.createRef() - const genderSelect = React.createRef() - const privateSelect = React.createRef() + // 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) - // useEffect(() => { - // if (cookies.user) setPicture(cookies.user.picture) - // if (cookies.user) setLanguage(cookies.user.language) - // if (cookies.user) setGender(cookies.user.gender) - // }, [cookies]) + // Setup - const pictureOptions = pictureData - .sort((a, b) => (a.name.en > b.name.en ? 1 : -1)) - .map((item, i) => { - return ( - - ) + // UI management + function openChange(open: boolean) { + setOpen(open) + } + + function openSelect(name: 'picture' | 'gender' | 'language' | 'theme') { + const stateVars = selectOpenState + Object.keys(stateVars).forEach((key) => { + if (key === name) { + stateVars[name] = true + } else { + stateVars[key] = false + } }) - function handlePictureChange(event: React.ChangeEvent) { - if (pictureSelect.current) setPicture(pictureSelect.current.value) + setSelectOpenState(stateVars) } - function handleLanguageChange(event: React.ChangeEvent) { - if (languageSelect.current) setLanguage(languageSelect.current.value) + // Event handlers + function handlePictureChange(value: string) { + setPicture(value) } - function handleGenderChange(event: React.ChangeEvent) { - if (genderSelect.current) setGender(parseInt(genderSelect.current.value)) + function handleLanguageChange(value: string) { + setLanguage(value) } - function handlePrivateChange(checked: boolean) { - setPrivateProfile(checked) + function handleGenderChange(value: string) { + setGender(parseInt(value)) } + function handleThemeChange(value: string) { + setTheme(value) + setAppTheme(value) + } + + // API calls function update(event: React.FormEvent) { event.preventDefault() @@ -78,172 +127,180 @@ const AccountModal = () => { element: pictureData.find((i) => i.filename === picture)?.element, language: language, gender: gender, - private: privateProfile, + theme: theme, + // private: privateProfile, }, } - // api.endpoints.users - // .update(cookies.account.user_id, object, headers) - // .then((response) => { - // const user = response.data.user + if (accountState.account.user) { + api.endpoints.users + .update(accountState.account.user?.id, object) + .then((response) => { + const user = response.data - // const cookieObj = { - // picture: user.picture.picture, - // element: user.picture.element, - // gender: user.gender, - // language: user.language, - // } + const cookieObj = { + picture: user.avatar.picture, + element: user.avatar.element, + gender: user.gender, + language: user.language, + theme: user.theme, + } - // setCookies("user", cookieObj, { path: "/" }) + setCookie('user', cookieObj, { path: '/' }) - // accountState.account.user = { - // id: user.id, - // username: user.username, - // picture: user.picture.picture, - // element: user.picture.element, - // gender: user.gender, - // } + 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(user.language) - // }) + setOpen(false) + changeLanguage(router, user.language) + }) + } } - function changeLanguage(newLanguage: string) { - // if (newLanguage !== router.locale) { - // setCookies("NEXT_LOCALE", newLanguage, { path: "/" }) - // router.push(router.asPath, undefined, { locale: newLanguage }) - // } - } + // Views + const pictureOptions = pictureData + .sort((a, b) => (a.name.en > b.name.en ? 1 : -1)) + .map((item, i) => { + return ( + + {item.name[locale]} + + ) + }) - function openChange(open: boolean) { - setOpen(open) + const pictureField = () => ( + openSelect('picture')} + onChange={handlePictureChange} + 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} + + ) + + const genderField = () => ( + openSelect('gender')} + onChange={handleGenderChange} + value={`${gender}`} + > + + {t('modals.settings.gender.gran')} + + + {t('modals.settings.gender.djeeta')} + + + ) + + const languageField = () => ( + openSelect('language')} + onChange={handleLanguageChange} + value={language} + > + + {t('modals.settings.language.english')} + + + {t('modals.settings.language.japanese')} + + + ) + + const themeField = () => ( + openSelect('theme')} + onChange={handleThemeChange} + 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 ( - - + +
  • {t('menu.settings')}
  • -
    - - event.preventDefault()} - > -
    -
    - - {t('modals.settings.title')} - - - @{account.user?.username} - -
    - - - - - + + +
    +
    + + {t('modals.settings.title')} + + @{username}
    + + + + + +
    -
    -
    -
    - -
    - -
    i.filename === picture)?.element - }`} - > - {picture ? ( - Profile preview - ) : ( - '' - )} -
    - - -
    -
    -
    - -
    - - -
    -
    -
    - -
    - - -
    -
    -
    - -

    - {t('modals.settings.descriptions.private')} -

    -
    - - - - -
    - -
    - +
  • {t('menu.logout')}
  • diff --git a/components/LoginModal/index.tsx b/components/LoginModal/index.tsx index e15c9008..e5d3eda6 100644 --- a/components/LoginModal/index.tsx +++ b/components/LoginModal/index.tsx @@ -16,6 +16,8 @@ import { DialogClose, } from '~components/Dialog' +import changeLanguage from '~utils/changeLanguage' + import CrossIcon from '~public/icons/Cross.svg' import './index.scss' @@ -127,37 +129,38 @@ const LoginModal = (props: Props) => { } function storeUserInfo(response: AxiosResponse) { + // Extract the user const user = response.data - const cookieObj: UserCookie = { - picture: user.avatar.picture, - element: user.avatar.element, - language: user.language, - gender: user.gender, - } - - setCookie('user', cookieObj, { path: '/' }) + // Set user data in the user cookie + setCookie( + 'user', + { + picture: user.avatar.picture, + element: user.avatar.element, + language: user.language, + gender: user.gender, + theme: user.theme, + }, + { path: '/' } + ) + // Set the user data in the account state accountState.account.user = { id: user.id, username: user.username, picture: user.avatar.picture, element: user.avatar.element, gender: user.gender, + language: user.language, + theme: user.theme, } console.log('Authorizing account...') accountState.account.authorized = true setOpen(false) - changeLanguage(user.language) - } - - function changeLanguage(newLanguage: string) { - if (newLanguage !== router.locale) { - setCookie('NEXT_LOCALE', newLanguage, { path: '/' }) - router.push(router.asPath, undefined, { locale: newLanguage }) - } + changeLanguage(router, user.language) } function openChange(open: boolean) { diff --git a/components/PictureSelectItem/index.scss b/components/PictureSelectItem/index.scss new file mode 100644 index 00000000..e510166d --- /dev/null +++ b/components/PictureSelectItem/index.scss @@ -0,0 +1,42 @@ +.SelectItem.Picture { + display: flex; + flex-direction: row; + gap: $unit; + align-items: center; + + .preview { + $diameter: $unit-4x; + border-radius: $unit-2x; + width: $diameter; + height: $diameter; + + img { + width: $diameter; + height: auto; + } + + &.fire { + background: $fire-bg-20; + } + + &.water { + background: $water-bg-20; + } + + &.wind { + background: $wind-bg-20; + } + + &.earth { + background: $earth-bg-20; + } + + &.dark { + background: $dark-bg-10; + } + + &.light { + background: $light-bg-20; + } + } +} diff --git a/components/PictureSelectItem/index.tsx b/components/PictureSelectItem/index.tsx new file mode 100644 index 00000000..629a4aa4 --- /dev/null +++ b/components/PictureSelectItem/index.tsx @@ -0,0 +1,35 @@ +import React, { ComponentProps } from 'react' +import * as Select from '@radix-ui/react-select' + +import './index.scss' +import classNames from 'classnames' + +interface Props extends ComponentProps<'div'> { + src: string[] + element: string + value: string +} + +const PictureSelectItem = React.forwardRef( + function selectItem({ children, ...props }, forwardedRef) { + return ( + +
    + {`${props.value}`} +
    + {children} +
    + ) + } +) + +export default PictureSelectItem diff --git a/components/Select/index.scss b/components/Select/index.scss index 5fdfd7c0..4bcfa38c 100644 --- a/components/Select/index.scss +++ b/components/Select/index.scss @@ -43,6 +43,10 @@ } } + &.Table { + min-width: $unit * 30; + } + .SelectIcon { display: flex; align-items: center; diff --git a/components/SelectTableField/index.scss b/components/SelectTableField/index.scss new file mode 100644 index 00000000..85e2777f --- /dev/null +++ b/components/SelectTableField/index.scss @@ -0,0 +1,69 @@ +.TableField { + align-items: center; + display: grid; + gap: $unit * 2; + grid-template-columns: 1fr auto; + + &.Image { + grid-template-columns: 1fr auto 1fr; + } + + .Left { + display: flex; + flex-direction: column; + gap: calc($unit / 2); + + label { + color: var(--text-tertiary); + font-size: $font-regular; + } + + p { + color: var(--text-secondary); + font-size: $font-small; + line-height: 1.1; + max-width: 300px; + + &.jp { + max-width: 270px; + } + } + } + + .preview { + $diameter: $unit * 6; + background-color: $grey-90; + border-radius: 999px; + height: $diameter; + width: $diameter; + + img { + height: $diameter; + width: $diameter; + } + + &.fire { + background: $fire-bg-20; + } + + &.water { + background: $water-bg-20; + } + + &.wind { + background: $wind-bg-20; + } + + &.earth { + background: $earth-bg-20; + } + + &.dark { + background: $dark-bg-10; + } + + &.light { + background: $light-bg-20; + } + } +} diff --git a/components/SelectTableField/index.tsx b/components/SelectTableField/index.tsx new file mode 100644 index 00000000..6ee80dd9 --- /dev/null +++ b/components/SelectTableField/index.tsx @@ -0,0 +1,68 @@ +import classNames from 'classnames' +import { useEffect, useState } from 'react' +import Select from '~components/Select' + +import './index.scss' + +interface Props { + name: string + label: string + description?: string + open: boolean + value?: string + className?: string + imageAlt?: string + imageClass?: string + imageSrc?: string[] + children: React.ReactNode + onClick: () => void + onChange: (value: string) => void +} + +const SelectTableField = (props: Props) => { + const [value, setValue] = useState('') + + useEffect(() => { + if (props.value) setValue(props.value) + }, [props.value]) + + const image = () => { + return props.imageSrc && props.imageSrc.length > 0 ? ( +
    + {props.imageAlt} +
    + ) : ( + '' + ) + } + + return ( +
    +
    +

    {props.label}

    +

    {props.description}

    +
    + + {image()} + +
    + +
    +
    + ) +} + +export default SelectTableField diff --git a/components/SignupModal/index.tsx b/components/SignupModal/index.tsx index 44d29b59..4db64904 100644 --- a/components/SignupModal/index.tsx +++ b/components/SignupModal/index.tsx @@ -102,27 +102,36 @@ const SignupModal = (props: Props) => { } function storeUserInfo(response: AxiosResponse) { + // Extract the user const user = response.data - const cookieObj: UserCookie = { - picture: user.avatar.picture, - element: user.avatar.element, - language: user.language, - gender: user.gender, - } - - // TODO: Set language - setCookie('user', cookieObj, { path: '/' }) + // Set user data in the user cookie + setCookie( + 'user', + { + picture: user.avatar.picture, + element: user.avatar.element, + language: user.language, + gender: user.gender, + theme: user.theme, + }, + { path: '/' } + ) + // Set the user data in the account state accountState.account.user = { id: user.id, username: user.username, picture: user.avatar.picture, element: user.avatar.element, gender: user.gender, + language: user.language, + theme: user.theme, } + console.log('Authorizing account...') accountState.account.authorized = true + setOpen(false) } diff --git a/components/WeaponGrid/index.tsx b/components/WeaponGrid/index.tsx index a051c7ed..33d9d32e 100644 --- a/components/WeaponGrid/index.tsx +++ b/components/WeaponGrid/index.tsx @@ -106,12 +106,8 @@ const WeaponGrid = (props: Props) => { .catch((error) => { const code = error.response.status const data = error.response.data - console.log(error.response) - - console.log(data, code) if (code === 422) { if (data.code === 'incompatible_weapon_for_position') { - console.log('Here') setShowIncompatibleAlert(true) } } @@ -339,7 +335,6 @@ const WeaponGrid = (props: Props) => { } const incompatibleAlert = () => { - console.log(t('alert.incompatible_weapon')) return showIncompatibleAlert ? ( { setUserToken() - if (cookie) { - console.log(`Logged in as user "${cookieData.username}"`) + if (accountCookie) { + console.log(`Logged in as user "${cookieData.account.username}"`) accountState.account.authorized = true accountState.account.user = { - id: cookieData.userId, - username: cookieData.username, - picture: '', - element: '', - gender: 0, + id: cookieData.account.userId, + username: cookieData.account.username, + picture: cookieData.user.picture, + element: cookieData.user.element, + gender: cookieData.user.gender, + language: cookieData.user.language, + theme: cookieData.user.theme, } } else { console.log(`You are not currently logged in.`) } - }, [cookie, cookieData]) + }, []) return ( diff --git a/pages/p/[party].tsx b/pages/p/[party].tsx index 0496d603..806637bc 100644 --- a/pages/p/[party].tsx +++ b/pages/p/[party].tsx @@ -7,6 +7,7 @@ import Party from '~components/Party' import { appState } from '~utils/appState' import { groupWeaponKeys } from '~utils/groupWeaponKeys' import organizeRaids from '~utils/organizeRaids' +import setUserToken from '~utils/setUserToken' import api from '~utils/api' import type { NextApiRequest, NextApiResponse } from 'next' @@ -48,38 +49,35 @@ export const getServerSidePaths = async () => { // prettier-ignore export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => { - // Cookies - const cookie = getCookie("account", { req, res }) - const accountData: AccountCookie = cookie - ? JSON.parse(cookie as string) - : null - - const headers = accountData - ? { headers: { Authorization: `Bearer ${accountData.token}` } } - : {} + // Set headers for server-side requests + setUserToken(req, res) let { raids, sortedRaids } = await api.endpoints.raids .getAll() .then((response) => organizeRaids(response.data)) let jobs = await api.endpoints.jobs - .getAll({ params: headers }) + .getAll() .then((response) => { return response.data }) - let jobSkills = await api.allJobSkills(headers).then((response) => response.data) + let jobSkills = await api + .allJobSkills() + .then((response) => response.data) let weaponKeys = await api.endpoints.weapon_keys .getAll() .then((response) => groupWeaponKeys(response.data)) - + let party: Party | null = null if (query.party) { - let response = await api.endpoints.parties.getOne({ id: query.party, params: headers }) + let response = await api.endpoints.parties.getOne({ + id: query.party + }) party = response.data.party } else { - console.log("No party code") + console.log('No party code') } return { @@ -90,7 +88,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex raids: raids, sortedRaids: sortedRaids, weaponKeys: weaponKeys, - ...(await serverSideTranslations(locale, ["common"])), + ...(await serverSideTranslations(locale, ['common'])), // Will be passed to the page component as props }, } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index ced95973..483638e1 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -170,9 +170,12 @@ "picture": "Picture", "language": "Language", "gender": "Main Character", - "private": "Private" + "private": "Private", + "theme": "Theme" }, "descriptions": { + "picture": "Displayed next to your name", + "gender": "The character shown on your teams", "private": "Hide your profile and prevent your grids from showing up in collections" }, "gender": { @@ -183,6 +186,11 @@ "english": "English", "japanese": "Japanese" }, + "theme": { + "light": "Light", + "dark": "Dark", + "system": "Follow system" + }, "buttons": { "confirm": "Save settings" } diff --git a/public/locales/ja/common.json b/public/locales/ja/common.json index 335e109e..3a5813b5 100644 --- a/public/locales/ja/common.json +++ b/public/locales/ja/common.json @@ -170,9 +170,12 @@ "picture": "プロフィール画像", "language": "言語", "gender": "主人公", - "private": "プライベート" + "private": "プライベート", + "theme": "表示" }, "descriptions": { + "picture": "名前の隣に表示する", + "gender": "編成で表示するキャラクター", "private": "プロフィールを隠し、編成をコレクションに表示されないようにします" }, "gender": { @@ -183,6 +186,11 @@ "english": "英語", "japanese": "日本語" }, + "theme": { + "light": "ライト", + "dark": "ダーク", + "system": "システム" + }, "buttons": { "confirm": "設定を保存する" } diff --git a/styles/variables.scss b/styles/variables.scss index 1a3a53a8..f0bec0fe 100644 --- a/styles/variables.scss +++ b/styles/variables.scss @@ -126,7 +126,7 @@ $menu--text--light: $grey-90; $menu--text--dark: $grey-50; $menu--separator--light: $grey-90; $menu--separator--dark: $grey-05; -$menu--item--bg--light--hover: $grey-90; +$menu--item--bg--light--hover: $grey-85; $menu--item--bg--dark--hover: $grey-00; $menu--text--light--hover: $grey-100; $menu--text--dark--hover: $grey-15; diff --git a/types/UserCookie.d.ts b/types/UserCookie.d.ts index df04aadc..15a6c840 100644 --- a/types/UserCookie.d.ts +++ b/types/UserCookie.d.ts @@ -3,4 +3,5 @@ interface UserCookie { element: string language: string gender: number + theme: string } diff --git a/utils/accountState.tsx b/utils/accountState.tsx index 5ad320d8..fe2d3249 100644 --- a/utils/accountState.tsx +++ b/utils/accountState.tsx @@ -1,19 +1,21 @@ import { proxy } from 'valtio' +export type UserState = { + id: string + username: string + picture: string + element: string + gender: number + language: string + theme: string +} + interface AccountState { [key: string]: any account: { authorized: boolean - user: - | { - id: string - username: string - picture: string - element: string - gender: number - } - | undefined + user: UserState | undefined } } diff --git a/utils/changeLanguage.tsx b/utils/changeLanguage.tsx new file mode 100644 index 00000000..ff719da6 --- /dev/null +++ b/utils/changeLanguage.tsx @@ -0,0 +1,12 @@ +import { setCookie } from 'cookies-next' +import { NextRouter } from 'next/router' + +export default function changeLanguage( + router: NextRouter, + newLanguage: string +) { + if (newLanguage !== router.locale) { + setCookie('NEXT_LOCALE', newLanguage, { path: '/' }) + router.push(router.asPath, undefined, { locale: newLanguage }) + } +} diff --git a/utils/groupWeaponKeys.tsx b/utils/groupWeaponKeys.tsx index cb31b824..763bd359 100644 --- a/utils/groupWeaponKeys.tsx +++ b/utils/groupWeaponKeys.tsx @@ -10,7 +10,6 @@ export type GroupedWeaponKeys = { } export function groupWeaponKeys(keys: WeaponKey[]) { - console.log(keys) const numGroups = Math.max.apply( Math, keys.map((key) => key.group) @@ -28,7 +27,5 @@ export function groupWeaponKeys(keys: WeaponKey[]) { groupedKeys[weaponKeyGroups[i].slug] = keys.filter((key) => key.group == i) } - console.log(groupedKeys) - return groupedKeys }