diff --git a/components/AccountModal/index.tsx b/components/AccountModal/index.tsx index d5337693..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/Header/index.tsx b/components/Header/index.tsx index b5b18e18..b94b7d60 100644 --- a/components/Header/index.tsx +++ b/components/Header/index.tsx @@ -5,22 +5,13 @@ import { useRouter } from 'next/router' import { useTranslation } from 'next-i18next' import classNames from 'classnames' import clonedeep from 'lodash.clonedeep' +import Link from 'next/link' import api from '~utils/api' import { accountState, initialAccountState } from '~utils/accountState' import { appState, initialAppState } from '~utils/appState' import capitalizeFirstLetter from '~utils/capitalizeFirstLetter' -import Button from '~components/Button' -import HeaderMenu from '~components/HeaderMenu' - -import AddIcon from '~public/icons/Add.svg' -import LinkIcon from '~public/icons/Link.svg' -import MenuIcon from '~public/icons/Menu.svg' -import ArrowIcon from '~public/icons/Arrow.svg' -import SaveIcon from '~public/icons/Save.svg' - -import './index.scss' import { DropdownMenu, DropdownMenuTrigger, @@ -29,11 +20,18 @@ import { DropdownMenuItem, DropdownMenuSeparator, } from '~components/DropdownMenuContent' -import Link from 'next/link' import LoginModal from '~components/LoginModal' import SignupModal from '~components/SignupModal' import AccountModal from '~components/AccountModal' import Toast from '~components/Toast' +import Button from '~components/Button' + +import LinkIcon from '~public/icons/Link.svg' +import MenuIcon from '~public/icons/Menu.svg' +import ArrowIcon from '~public/icons/Arrow.svg' +import SaveIcon from '~public/icons/Save.svg' + +import './index.scss' const Header = () => { // Localization @@ -44,6 +42,9 @@ const Header = () => { // State management const [copyToastOpen, setCopyToastOpen] = useState(false) + const [loginModalOpen, setLoginModalOpen] = useState(false) + const [signupModalOpen, setSignupModalOpen] = useState(false) + const [settingsModalOpen, setSettingsModalOpen] = useState(false) const [leftMenuOpen, setLeftMenuOpen] = useState(false) const [rightMenuOpen, setRightMenuOpen] = useState(false) @@ -82,6 +83,10 @@ const Header = () => { setRightMenuOpen(false) } + function handleSettingsOpenChanged(open: boolean) { + setRightMenuOpen(false) + } + function copyToClipboard() { const el = document.createElement('input') el.value = window.location.href @@ -238,6 +243,34 @@ const Header = () => { ) } + const settingsModal = () => { + const user = accountState.account.user + + if (user) { + return ( + + ) + } + } + + const loginModal = () => { + return + } + + const signupModal = () => { + return ( + + ) + } + const left = () => { return (
@@ -374,16 +407,9 @@ const Header = () => { setSettingsModalOpen(true)} > - + {t('menu.settings')} {t('menu.logout')} @@ -405,8 +431,18 @@ const Header = () => { - - + setLoginModalOpen(true)} + > + Log in + + setSignupModalOpen(true)} + > + Sign up + ) @@ -420,6 +456,9 @@ const Header = () => { {left()} {right()} {urlCopyToast()} + {settingsModal()} + {loginModal()} + {signupModal()} ) } diff --git a/components/LoginModal/index.tsx b/components/LoginModal/index.tsx index e2910acf..f42e1ed3 100644 --- a/components/LoginModal/index.tsx +++ b/components/LoginModal/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { setCookie } from 'cookies-next' import { useRouter } from 'next/router' import { useTranslation } from 'react-i18next' @@ -26,7 +26,12 @@ interface ErrorMap { const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ -const LoginModal = () => { +interface Props { + open: boolean + onOpenChange?: (open: boolean) => void +} + +const LoginModal = (props: Props) => { const router = useRouter() const { t } = useTranslation('common') @@ -46,6 +51,10 @@ const LoginModal = () => { const footerRef: React.RefObject = React.createRef() const form: React.RefObject[] = [emailInput, passwordInput] + useEffect(() => { + setOpen(props.open) + }, [props.open]) + function handleChange(event: React.ChangeEvent) { const { name, value } = event.target let newErrors = { ...errors } @@ -185,6 +194,8 @@ const LoginModal = () => { email: '', password: '', }) + + if (props.onOpenChange) props.onOpenChange(open) } function onEscapeKeyDown(event: KeyboardEvent) { @@ -198,11 +209,6 @@ const LoginModal = () => { return ( - -
- {t('menu.login')} -
-
void +} interface ErrorMap { [index: string]: string @@ -58,6 +61,10 @@ const SignupModal = (props: Props) => { passwordConfirmationInput, ] + useEffect(() => { + setOpen(props.open) + }, [props.open]) + function register(event: React.FormEvent) { event.preventDefault() @@ -266,6 +273,8 @@ const SignupModal = (props: Props) => { password: '', passwordConfirmation: '', }) + + if (props.onOpenChange) props.onOpenChange(open) } function onEscapeKeyDown(event: KeyboardEvent) { @@ -279,11 +288,6 @@ const SignupModal = (props: Props) => { return ( - -
- {t('menu.signup')} -
-