Adds AccountModal component
This commit is contained in:
parent
053522a81d
commit
4e701572ab
2 changed files with 318 additions and 0 deletions
160
components/AccountModal/index.scss
Normal file
160
components/AccountModal/index.scss
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
.Account.Dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit * 2;
|
||||
width: $unit * 60;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit * 2;
|
||||
|
||||
.Switch {
|
||||
$height: 34px;
|
||||
background: $grey-70;
|
||||
border-radius: $height / 2;
|
||||
border: none;
|
||||
position: relative;
|
||||
width: 58px;
|
||||
height: $height;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px $grey-00;
|
||||
}
|
||||
|
||||
&[data-state="checked"] {
|
||||
background: $grey-00;
|
||||
}
|
||||
}
|
||||
|
||||
.Thumb {
|
||||
background: white;
|
||||
border-radius: 13px;
|
||||
display: block;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
transition: transform 100ms;
|
||||
transform: translateX(-1px);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&[data-state="checked"] {
|
||||
background: white;
|
||||
transform: translateX(21px);
|
||||
}
|
||||
}
|
||||
|
||||
.Button {
|
||||
font-size: $font-regular;
|
||||
padding: ($unit * 1.5) ($unit * 2);
|
||||
margin-top: $unit * 2;
|
||||
width: 100%;
|
||||
|
||||
&.btn-disabled {
|
||||
background: $grey-90;
|
||||
color: $grey-70;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:not(.btn-disabled) {
|
||||
background: $grey-90;
|
||||
color: $grey-40;
|
||||
|
||||
&:hover {
|
||||
background: $grey-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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: $unit / 2;
|
||||
|
||||
label {
|
||||
color: $grey-00;
|
||||
font-size: $font-regular;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $grey-60;
|
||||
font-size: $font-small;
|
||||
line-height: 1.1;
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
$diameter: 48px;
|
||||
background-color: $grey-90;
|
||||
border-radius: 999px;
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
|
||||
img {
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
|
||||
&.fire {
|
||||
background: $fire-bg-light;
|
||||
}
|
||||
|
||||
&.water {
|
||||
background: $water-bg-light;
|
||||
}
|
||||
|
||||
&.wind {
|
||||
background: $wind-bg-light;
|
||||
}
|
||||
|
||||
&.earth {
|
||||
background: $earth-bg-light;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background: $dark-bg-light;
|
||||
}
|
||||
|
||||
&.light {
|
||||
background: $light-bg-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: $unit;
|
||||
|
||||
h2 {
|
||||
margin-bottom: $unit * 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.DialogDescription {
|
||||
font-size: $font-regular;
|
||||
line-height: 1.24;
|
||||
margin-bottom: $unit;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
components/AccountModal/index.tsx
Normal file
158
components/AccountModal/index.tsx
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
import * as Switch from '@radix-ui/react-switch'
|
||||
|
||||
import api from '~utils/api'
|
||||
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'
|
||||
|
||||
const AccountModal = () => {
|
||||
const { account } = useSnapshot(accountState)
|
||||
|
||||
// Cookies
|
||||
const [cookies] = useCookies(['user'])
|
||||
const headers = (cookies.user != null) ? {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${cookies.user.access_token}`
|
||||
}
|
||||
} : {}
|
||||
|
||||
// State
|
||||
const [open, setOpen] = useState(false)
|
||||
const [picture, setPicture] = useState('')
|
||||
const [language, setLanguage] = useState('')
|
||||
const [privateProfile, setPrivateProfile] = useState(false)
|
||||
|
||||
// Refs
|
||||
const pictureSelect = React.createRef<HTMLSelectElement>()
|
||||
const languageSelect = React.createRef<HTMLSelectElement>()
|
||||
const privateSelect = React.createRef<HTMLInputElement>()
|
||||
|
||||
useEffect(() => {
|
||||
if (cookies.user) setPicture(cookies.user.picture)
|
||||
if (cookies.user) setLanguage(cookies.user.language)
|
||||
}, [cookies])
|
||||
|
||||
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.en}</option>
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
function handlePictureChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
if (pictureSelect.current)
|
||||
setPicture(pictureSelect.current.value)
|
||||
}
|
||||
|
||||
function handleLanguageChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
if (languageSelect.current)
|
||||
setLanguage(languageSelect.current.value)
|
||||
}
|
||||
|
||||
function handlePrivateChange(checked: boolean) {
|
||||
setPrivateProfile(checked)
|
||||
}
|
||||
|
||||
function update(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
|
||||
const object = {
|
||||
user: {
|
||||
picture: picture,
|
||||
element: pictureData.find(i => i.filename === picture)?.element,
|
||||
language: language,
|
||||
private: privateProfile
|
||||
}
|
||||
}
|
||||
|
||||
api.endpoints.users.update(cookies.user.user_id, object, headers)
|
||||
.then(response => {
|
||||
setOpen(false)
|
||||
})
|
||||
}
|
||||
|
||||
function openChange(open: boolean) {
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>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">Account Settings</Dialog.Title>
|
||||
<Dialog.Title className="DialogTitle">@{account.user?.username}</Dialog.Title>
|
||||
</div>
|
||||
<Dialog.Close className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
<form onSubmit={update}>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>Picture</label>
|
||||
</div>
|
||||
|
||||
<div className={`preview ${pictureData.find(i => i.filename === picture)?.element}`}>
|
||||
<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>Language</label>
|
||||
</div>
|
||||
|
||||
<select name="language" onChange={handleLanguageChange} value={language} ref={languageSelect}>
|
||||
<option key="en" value="en">English</option>
|
||||
<option key="jp" value="jp">Japanese</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>Private</label>
|
||||
<p>Hide your profile and prevent your grids from showing up in collections</p>
|
||||
</div>
|
||||
|
||||
<Switch.Root className="Switch" onCheckedChange={handlePrivateChange} checked={privateProfile}>
|
||||
<Switch.Thumb className="Thumb" />
|
||||
</Switch.Root>
|
||||
</div>
|
||||
|
||||
<Button>Save settings</Button>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountModal
|
||||
Loading…
Reference in a new issue