Merge pull request #51 from jedmund/user-settings
Refactor user settings
This commit is contained in:
commit
d64e9824c0
23 changed files with 589 additions and 363 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<StateVariables>({
|
||||
picture: false,
|
||||
gender: false,
|
||||
language: false,
|
||||
theme: false,
|
||||
})
|
||||
|
||||
// Refs
|
||||
const pictureSelect = React.createRef<HTMLSelectElement>()
|
||||
const languageSelect = React.createRef<HTMLSelectElement>()
|
||||
const genderSelect = React.createRef<HTMLSelectElement>()
|
||||
const privateSelect = React.createRef<HTMLInputElement>()
|
||||
// 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 (
|
||||
<option key={`picture-${i}`} value={item.filename}>
|
||||
{item.name[locale]}
|
||||
</option>
|
||||
)
|
||||
// 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<HTMLSelectElement>) {
|
||||
if (pictureSelect.current) setPicture(pictureSelect.current.value)
|
||||
setSelectOpenState(stateVars)
|
||||
}
|
||||
|
||||
function handleLanguageChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
if (languageSelect.current) setLanguage(languageSelect.current.value)
|
||||
// Event handlers
|
||||
function handlePictureChange(value: string) {
|
||||
setPicture(value)
|
||||
}
|
||||
|
||||
function handleGenderChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
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<HTMLFormElement>) {
|
||||
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 (
|
||||
<PictureSelectItem
|
||||
key={`picture-${i}`}
|
||||
element={item.element}
|
||||
src={[
|
||||
`/profile/${item.filename}.png`,
|
||||
`/profile/${item.filename}@2x.png 2x`,
|
||||
]}
|
||||
value={item.filename}
|
||||
>
|
||||
{item.name[locale]}
|
||||
</PictureSelectItem>
|
||||
)
|
||||
})
|
||||
|
||||
function openChange(open: boolean) {
|
||||
setOpen(open)
|
||||
const pictureField = () => (
|
||||
<SelectTableField
|
||||
name="picture"
|
||||
description={t('modals.settings.descriptions.picture')}
|
||||
className="Image"
|
||||
label={t('modals.settings.labels.picture')}
|
||||
open={selectOpenState.picture}
|
||||
onClick={() => 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}
|
||||
</SelectTableField>
|
||||
)
|
||||
|
||||
const genderField = () => (
|
||||
<SelectTableField
|
||||
name="gender"
|
||||
description={t('modals.settings.descriptions.gender')}
|
||||
label={t('modals.settings.labels.gender')}
|
||||
open={selectOpenState.gender}
|
||||
onClick={() => openSelect('gender')}
|
||||
onChange={handleGenderChange}
|
||||
value={`${gender}`}
|
||||
>
|
||||
<SelectItem key="gran" value="0">
|
||||
{t('modals.settings.gender.gran')}
|
||||
</SelectItem>
|
||||
<SelectItem key="djeeta" value="1">
|
||||
{t('modals.settings.gender.djeeta')}
|
||||
</SelectItem>
|
||||
</SelectTableField>
|
||||
)
|
||||
|
||||
const languageField = () => (
|
||||
<SelectTableField
|
||||
name="language"
|
||||
label={t('modals.settings.labels.language')}
|
||||
open={selectOpenState.language}
|
||||
onClick={() => openSelect('language')}
|
||||
onChange={handleLanguageChange}
|
||||
value={language}
|
||||
>
|
||||
<SelectItem key="en" value="en">
|
||||
{t('modals.settings.language.english')}
|
||||
</SelectItem>
|
||||
<SelectItem key="ja" value="ja">
|
||||
{t('modals.settings.language.japanese')}
|
||||
</SelectItem>
|
||||
</SelectTableField>
|
||||
)
|
||||
|
||||
const themeField = () => (
|
||||
<SelectTableField
|
||||
name="theme"
|
||||
label={t('modals.settings.labels.theme')}
|
||||
open={selectOpenState.theme}
|
||||
onClick={() => openSelect('theme')}
|
||||
onChange={handleThemeChange}
|
||||
value={theme}
|
||||
>
|
||||
<SelectItem key="system" value="system">
|
||||
{t('modals.settings.theme.system')}
|
||||
</SelectItem>
|
||||
<SelectItem key="light" value="light">
|
||||
{t('modals.settings.theme.light')}
|
||||
</SelectItem>
|
||||
<SelectItem key="dark" value="dark">
|
||||
{t('modals.settings.theme.dark')}
|
||||
</SelectItem>
|
||||
</SelectTableField>
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>
|
||||
<Dialog open={open} onOpenChange={openChange}>
|
||||
<DialogTrigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t('menu.settings')}</span>
|
||||
</li>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content
|
||||
className="Account Dialog"
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
>
|
||||
<div className="DialogHeader">
|
||||
<div className="DialogTop">
|
||||
<Dialog.Title className="SubTitle">
|
||||
{t('modals.settings.title')}
|
||||
</Dialog.Title>
|
||||
<Dialog.Title className="DialogTitle">
|
||||
@{account.user?.username}
|
||||
</Dialog.Title>
|
||||
</div>
|
||||
<Dialog.Close className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</Dialog.Close>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="Account Dialog">
|
||||
<div className="DialogHeader">
|
||||
<div className="DialogTop">
|
||||
<DialogTitle className="SubTitle">
|
||||
{t('modals.settings.title')}
|
||||
</DialogTitle>
|
||||
<DialogTitle className="DialogTitle">@{username}</DialogTitle>
|
||||
</div>
|
||||
<DialogClose className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</DialogClose>
|
||||
</div>
|
||||
|
||||
<form onSubmit={update}>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>{t('modals.settings.labels.picture')}</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`preview ${
|
||||
pictureData.find((i) => i.filename === picture)?.element
|
||||
}`}
|
||||
>
|
||||
{picture ? (
|
||||
<img
|
||||
alt="Profile preview"
|
||||
srcSet={`/profile/${picture}.png,
|
||||
/profile/${picture}@2x.png 2x`}
|
||||
src={`/profile/${picture}.png`}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
|
||||
<select
|
||||
name="picture"
|
||||
onChange={handlePictureChange}
|
||||
value={picture}
|
||||
ref={pictureSelect}
|
||||
>
|
||||
{pictureOptions}
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>{t('modals.settings.labels.gender')}</label>
|
||||
</div>
|
||||
|
||||
<select
|
||||
name="gender"
|
||||
onChange={handleGenderChange}
|
||||
value={gender}
|
||||
ref={genderSelect}
|
||||
>
|
||||
<option key="gran" value="0">
|
||||
{t('modals.settings.gender.gran')}
|
||||
</option>
|
||||
<option key="djeeta" value="1">
|
||||
{t('modals.settings.gender.djeeta')}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>{t('modals.settings.labels.language')}</label>
|
||||
</div>
|
||||
|
||||
<select
|
||||
name="language"
|
||||
onChange={handleLanguageChange}
|
||||
value={language}
|
||||
ref={languageSelect}
|
||||
>
|
||||
<option key="en" value="en">
|
||||
{t('modals.settings.language.english')}
|
||||
</option>
|
||||
<option key="jp" value="ja">
|
||||
{t('modals.settings.language.japanese')}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>{t('modals.settings.labels.private')}</label>
|
||||
<p className={locale}>
|
||||
{t('modals.settings.descriptions.private')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Switch.Root
|
||||
className="Switch"
|
||||
onCheckedChange={handlePrivateChange}
|
||||
checked={privateProfile}
|
||||
>
|
||||
<Switch.Thumb className="Thumb" />
|
||||
</Switch.Root>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
contained={true}
|
||||
text={t('modals.settings.buttons.confirm')}
|
||||
/>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
<form onSubmit={update}>
|
||||
{pictureField()}
|
||||
{genderField()}
|
||||
{languageField()}
|
||||
{themeField()}
|
||||
<Button
|
||||
contained={true}
|
||||
text={t('modals.settings.buttons.confirm')}
|
||||
/>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,13 +107,6 @@ const AXSelect = (props: Props) => {
|
|||
}, [props.currentSkills, setSecondaryAxModifier])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
primaryAxModifier,
|
||||
primaryAxValue,
|
||||
secondaryAxModifier,
|
||||
secondaryAxValue
|
||||
)
|
||||
|
||||
let noErrors = false
|
||||
|
||||
if (errors.axValue1 === '' && errors.axValue2 === '') {
|
||||
|
|
@ -132,13 +125,6 @@ const AXSelect = (props: Props) => {
|
|||
secondaryAxValue > 0
|
||||
)
|
||||
noErrors = true
|
||||
else
|
||||
console.log(
|
||||
primaryAxModifier >= 0,
|
||||
primaryAxValue > 0,
|
||||
secondaryAxModifier >= 0,
|
||||
secondaryAxValue > 0
|
||||
)
|
||||
}
|
||||
|
||||
props.sendValidity(noErrors)
|
||||
|
|
|
|||
|
|
@ -35,5 +35,6 @@ export const DialogContent = React.forwardRef<HTMLDivElement, Props>(
|
|||
)
|
||||
|
||||
export const Dialog = DialogPrimitive.Root
|
||||
export const DialogTitle = DialogPrimitive.Title
|
||||
export const DialogTrigger = DialogPrimitive.Trigger
|
||||
export const DialogClose = DialogPrimitive.Close
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
display: none;
|
||||
min-width: 220px;
|
||||
position: absolute;
|
||||
top: $unit * 5; // This shouldn't be hardcoded. How to calculate it?
|
||||
top: $unit * 5.75; // This shouldn't be hardcoded. How to calculate it?
|
||||
// Also, add space that doesn't make the menu disappear if you move your mouse slowly
|
||||
z-index: 10;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import LoginModal from '~components/LoginModal'
|
|||
import SignupModal from '~components/SignupModal'
|
||||
|
||||
import './index.scss'
|
||||
import { accountState } from '~utils/accountState'
|
||||
|
||||
interface Props {
|
||||
authenticated: boolean
|
||||
|
|
@ -59,8 +60,8 @@ const HeaderMenu = (props: Props) => {
|
|||
<span>{accountData.username}</span>
|
||||
<img
|
||||
alt={userData.picture}
|
||||
className={`profile ${userData.element}`}
|
||||
srcSet={`/profile/${userData.picture}.png,
|
||||
className={`profile ${accountState.account.user?.element}`}
|
||||
srcSet={`/profile/${accountState.account.user?.picture}.png,
|
||||
/profile/${userData.picture}@2x.png 2x`}
|
||||
src={`/profile/${userData.picture}.png`}
|
||||
/>
|
||||
|
|
@ -85,7 +86,13 @@ const HeaderMenu = (props: Props) => {
|
|||
</div>
|
||||
<div className="MenuGroup">
|
||||
<AboutModal />
|
||||
<AccountModal />
|
||||
<AccountModal
|
||||
username={accountState.account.user?.username}
|
||||
picture={accountState.account.user?.picture}
|
||||
gender={accountState.account.user?.gender}
|
||||
language={accountState.account.user?.language}
|
||||
theme={accountState.account.user?.theme}
|
||||
/>
|
||||
<li className="MenuItem" onClick={props.logout}>
|
||||
<span>{t('menu.logout')}</span>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
42
components/PictureSelectItem/index.scss
Normal file
42
components/PictureSelectItem/index.scss
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
components/PictureSelectItem/index.tsx
Normal file
35
components/PictureSelectItem/index.tsx
Normal file
|
|
@ -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<HTMLDivElement, Props>(
|
||||
function selectItem({ children, ...props }, forwardedRef) {
|
||||
return (
|
||||
<Select.Item
|
||||
className={classNames('SelectItem Picture', props.className)}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
value={`${props.value}`}
|
||||
>
|
||||
<div className={`preview ${props.element}`}>
|
||||
<img
|
||||
alt={`${props.value}`}
|
||||
src={props.src[0]}
|
||||
srcSet={props.src.join(', ')}
|
||||
/>
|
||||
</div>
|
||||
<Select.ItemText>{children}</Select.ItemText>
|
||||
</Select.Item>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default PictureSelectItem
|
||||
|
|
@ -43,6 +43,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.Table {
|
||||
min-width: $unit * 30;
|
||||
}
|
||||
|
||||
.SelectIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
69
components/SelectTableField/index.scss
Normal file
69
components/SelectTableField/index.scss
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
components/SelectTableField/index.tsx
Normal file
68
components/SelectTableField/index.tsx
Normal file
|
|
@ -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 ? (
|
||||
<div className={`preview ${props.imageClass}`}>
|
||||
<img
|
||||
alt={props.imageAlt}
|
||||
srcSet={props.imageSrc.join(', ')}
|
||||
src={props.imageSrc[0]}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames({ TableField: true }, props.className)}>
|
||||
<div className="Left">
|
||||
<h3>{props.label}</h3>
|
||||
<p>{props.description}</p>
|
||||
</div>
|
||||
|
||||
{image()}
|
||||
|
||||
<div className="Right">
|
||||
<Select
|
||||
name={props.name}
|
||||
open={props.open}
|
||||
onClick={props.onClick}
|
||||
onValueChange={props.onChange}
|
||||
triggerClass={classNames({ Bound: true, Table: true })}
|
||||
value={value}
|
||||
>
|
||||
{props.children}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectTableField
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<Alert
|
||||
open={showIncompatibleAlert}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect } from 'react'
|
||||
import { getCookie } from 'cookies-next'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getCookie, getCookies } from 'cookies-next'
|
||||
import { appWithTranslation } from 'next-i18next'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { ThemeProvider, useTheme } from 'next-themes'
|
||||
|
||||
import type { AppProps } from 'next/app'
|
||||
import Layout from '~components/Layout'
|
||||
|
|
@ -12,27 +12,34 @@ import setUserToken from '~utils/setUserToken'
|
|||
import '../styles/globals.scss'
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
const cookie = getCookie('account')
|
||||
const cookieData: AccountCookie = cookie ? JSON.parse(cookie as string) : null
|
||||
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,
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
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 (
|
||||
<ThemeProvider>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "設定を保存する"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
1
types/UserCookie.d.ts
vendored
1
types/UserCookie.d.ts
vendored
|
|
@ -3,4 +3,5 @@ interface UserCookie {
|
|||
element: string
|
||||
language: string
|
||||
gender: number
|
||||
theme: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
12
utils/changeLanguage.tsx
Normal file
12
utils/changeLanguage.tsx
Normal file
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue