-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add account page to change password
- Loading branch information
tcsola
authored and
tcsola
committed
Sep 26, 2024
1 parent
d428b46
commit 896ae1d
Showing
12 changed files
with
690 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { useMeFetcher } from "@reearth/services/api"; | ||
import { useCallback } from "react"; | ||
|
||
export type UpdatePasswordType = { | ||
password: string; | ||
passwordConfirmation: string; | ||
}; | ||
|
||
export default () => { | ||
const { useMeQuery, useUpdatePassword, useDeleteUser } = useMeFetcher(); | ||
|
||
const passwordPolicy = window.REEARTH_CONFIG?.passwordPolicy; | ||
|
||
const { me: data } = useMeQuery(); | ||
|
||
const handleUpdateUserPassword = useCallback( | ||
async ({ password, passwordConfirmation }: UpdatePasswordType) => { | ||
await useUpdatePassword({ password, passwordConfirmation }); | ||
}, | ||
[useUpdatePassword] | ||
); | ||
|
||
const handleDeleteUser = useCallback(async () => { | ||
const userId = data.id; | ||
if (userId) await useDeleteUser({ userId }); | ||
}, [data.id, useDeleteUser]); | ||
|
||
return { | ||
meData: data, | ||
passwordPolicy, | ||
handleUpdateUserPassword, | ||
handleDeleteUser | ||
}; | ||
}; |
138 changes: 138 additions & 0 deletions
138
web/src/beta/features/AccountAndWorkSpaceSetting/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import { | ||
DEFAULT_SIDEBAR_WIDTH, | ||
SidebarMenuItem, | ||
SidebarSection, | ||
SidebarVersion, | ||
SidebarWrapper | ||
} from "@reearth/beta/ui/components/Sidebar"; | ||
import { useT } from "@reearth/services/i18n"; | ||
import { styled } from "@reearth/services/theme"; | ||
import { FC, useMemo } from "react"; | ||
|
||
import Navbar from "../Navbar"; | ||
|
||
import useHook from "./hooks"; | ||
import AccountSetting from "./innerPages/AccountSetting"; | ||
|
||
type Props = { | ||
sceneId?: string; | ||
projectId?: string; | ||
workspaceId?: string; | ||
tab: string; | ||
}; | ||
|
||
export const accountSettingTabs = [ | ||
{ id: "account", text: "Account", icon: "user" }, | ||
{ id: "workspace", text: "Workspace", icon: "users" }, | ||
{ id: "members", text: "Members", icon: "usersFour" } | ||
] as const; | ||
|
||
const AccountAndWorkSpaceSetting: FC<Props> = ({ tab }) => { | ||
const t = useT(); | ||
const tabs = useMemo( | ||
() => | ||
accountSettingTabs.map((tab) => ({ | ||
id: tab.id, | ||
icon: tab.icon, | ||
text: t(tab.text), | ||
path: `/settings/${tab.id}` | ||
})), | ||
[t] | ||
); | ||
const { meData, passwordPolicy, handleUpdateUserPassword } = useHook(); | ||
console.log(meData); | ||
const { name, email } = meData; | ||
|
||
return ( | ||
<Wrapper> | ||
<Navbar | ||
// projectId={projectId} | ||
workspaceId={meData.myTeam?.id} | ||
// sceneId={sceneId} | ||
page="settings" | ||
/> | ||
<MainSection> | ||
<LeftSidePanel> | ||
<SidebarWrapper> | ||
<SidebarSection> | ||
{tabs?.map((t) => ( | ||
<SidebarMenuItem | ||
key={t.id} | ||
path={t.path} | ||
text={t.text} | ||
active={t.id === tab} | ||
icon={t.icon} | ||
/> | ||
))} | ||
</SidebarSection> | ||
<SidebarVersion /> | ||
</SidebarWrapper> | ||
</LeftSidePanel> | ||
<Content> | ||
{tab === "account" && ( | ||
<AccountSetting | ||
onUpdateUserPassword={handleUpdateUserPassword} | ||
passwordPolicy={passwordPolicy} | ||
imformationData={{ name, email }} | ||
/> | ||
)} | ||
</Content> | ||
</MainSection> | ||
</Wrapper> | ||
); | ||
}; | ||
|
||
const Wrapper = styled("div")(({ theme }) => ({ | ||
display: "flex", | ||
flexDirection: "column", | ||
height: "100%", | ||
width: "100%", | ||
color: theme.content.main, | ||
backgroundColor: theme.bg[0], | ||
["*"]: { | ||
boxSizing: "border-box" | ||
}, | ||
["* ::-webkit-scrollbar"]: { | ||
width: "8px" | ||
}, | ||
["* ::-webkit-scrollbar-track"]: { | ||
background: theme.relative.darker, | ||
borderRadius: "10px" | ||
}, | ||
["* ::-webkit-scrollbar-thumb"]: { | ||
background: theme.relative.light, | ||
borderRadius: "4px" | ||
}, | ||
["* ::-webkit-scrollbar-thumb:hover"]: { | ||
background: theme.relative.lighter | ||
} | ||
})); | ||
|
||
const MainSection = styled("div")(() => ({ | ||
display: "flex", | ||
flex: 1, | ||
overflow: "auto", | ||
position: "relative" | ||
})); | ||
|
||
const LeftSidePanel = styled("div")(({ theme }) => ({ | ||
width: DEFAULT_SIDEBAR_WIDTH, | ||
height: "100%", | ||
backgroundColor: theme.bg[1], | ||
display: "flex", | ||
padding: `${theme.spacing.large}px 0`, | ||
boxSizing: "border-box" | ||
})); | ||
|
||
const Content = styled("div")(({ theme }) => ({ | ||
position: "relative", | ||
display: "flex", | ||
flexDirection: "column", | ||
width: "100%", | ||
height: "100%", | ||
alignItems: "center", | ||
overflow: "auto", | ||
padding: `${theme.spacing.super}px` | ||
})); | ||
|
||
export default AccountAndWorkSpaceSetting; |
211 changes: 211 additions & 0 deletions
211
...eta/features/AccountAndWorkSpaceSetting/innerPages/AccountSetting/PasswordModal/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
import { Flex } from "@aws-amplify/ui-react"; | ||
import { | ||
Modal, | ||
Button, | ||
ModalPanel, | ||
Typography, | ||
TextInput, | ||
Icon | ||
} from "@reearth/beta/lib/reearth-ui"; | ||
import { metricsSizes } from "@reearth/beta/utils/metrics"; | ||
import { useT } from "@reearth/services/i18n"; | ||
import { styled } from "@reearth/services/theme"; | ||
import React, { useState, useCallback, useEffect } from "react"; | ||
|
||
export type PasswordPolicy = { | ||
tooShort?: RegExp; | ||
tooLong?: RegExp; | ||
whitespace?: RegExp; | ||
lowSecurity?: RegExp; | ||
medSecurity?: RegExp; | ||
highSecurity?: RegExp; | ||
}; | ||
|
||
type Props = { | ||
className?: string; | ||
project?: { | ||
id: string; | ||
name: string; | ||
isArchived: boolean; | ||
}; | ||
workspace?: { | ||
id: string; | ||
name: string; | ||
}; | ||
isVisible: boolean; | ||
archiveProject?: (archived: boolean) => void; | ||
onClose?: () => void; | ||
passwordPolicy?: PasswordPolicy; | ||
updatePassword?: ({ | ||
password, | ||
passwordConfirmation | ||
}: { | ||
password: string; | ||
passwordConfirmation: string; | ||
}) => void; | ||
}; | ||
|
||
const PasswordModal: React.FC<Props> = ({ | ||
isVisible, | ||
onClose, | ||
passwordPolicy, | ||
updatePassword | ||
}) => { | ||
const t = useT(); | ||
|
||
const [password, setPassword] = useState(""); | ||
const [regexMessage, setRegexMessage] = useState<string | undefined | null>(); | ||
const [passwordConfirmation, setPasswordConfirmation] = useState<string>(); | ||
const [disabled, setDisabled] = useState(true); | ||
|
||
const handlePasswordChange = useCallback( | ||
(password: string | undefined) => { | ||
setPassword(password ?? ""); | ||
switch (true) { | ||
case passwordPolicy?.whitespace?.test(password ?? ""): | ||
setRegexMessage(t("No whitespace is allowed.")); | ||
break; | ||
case passwordPolicy?.tooShort?.test(password ?? ""): | ||
setRegexMessage(t("Too short.")); | ||
break; | ||
case passwordPolicy?.tooLong?.test(password ?? ""): | ||
setRegexMessage(t("That is terribly long.")); | ||
break; | ||
case passwordPolicy?.highSecurity?.test(password ?? ""): | ||
setRegexMessage(t("That password is great!")); | ||
break; | ||
case passwordPolicy?.medSecurity?.test(password ?? ""): | ||
setRegexMessage(t("That password is better.")); | ||
break; | ||
case passwordPolicy?.lowSecurity?.test(password ?? ""): | ||
setRegexMessage(t("That password is okay.")); | ||
break; | ||
default: | ||
setRegexMessage(t("That password confuses me, but might be okay.")); | ||
break; | ||
} | ||
}, | ||
[t, password] // eslint-disable-line react-hooks/exhaustive-deps | ||
); | ||
|
||
const handleClose = useCallback(() => { | ||
setPassword(""); | ||
setPasswordConfirmation(""); | ||
onClose?.(); | ||
}, [onClose]); | ||
|
||
const handleSave = useCallback(() => { | ||
if (password === passwordConfirmation) { | ||
updatePassword?.({ password, passwordConfirmation }); | ||
handleClose(); | ||
} | ||
}, [updatePassword, handleClose, password, passwordConfirmation]); | ||
|
||
useEffect(() => { | ||
if ( | ||
password !== passwordConfirmation || | ||
(passwordPolicy?.highSecurity && | ||
!passwordPolicy.highSecurity.test(password)) || | ||
passwordPolicy?.tooShort?.test(password) || | ||
passwordPolicy?.tooLong?.test(password) | ||
) { | ||
setDisabled(true); | ||
} else { | ||
setDisabled(false); | ||
} | ||
}, [password, passwordConfirmation, passwordPolicy]); | ||
|
||
const isMatchPassword = | ||
password !== passwordConfirmation && passwordConfirmation; | ||
|
||
return ( | ||
<Modal size="small" visible={isVisible}> | ||
<ModalPanel | ||
title={t("Change password")} | ||
onCancel={handleClose} | ||
actions={[ | ||
<Button | ||
key="Change password" | ||
title={t("Change password")} | ||
appearance="dangerous" | ||
disabled={disabled} | ||
onClick={handleSave} | ||
/> | ||
]} | ||
> | ||
<ModalContentWrapper> | ||
<div> | ||
<SubText> | ||
<Typography size="body" weight="bold"> | ||
{t( | ||
`In order to protect your account, make sure your password is unique and strong.` | ||
)} | ||
</Typography> | ||
</SubText> | ||
<PasswordField direction="column"> | ||
<Typography size="body" weight="bold"> | ||
{t("New password")} | ||
</Typography> | ||
<TextInput | ||
value={password} | ||
onChange={handlePasswordChange} | ||
type="password" | ||
/> | ||
{password ? ( | ||
<PasswordMessage size="body" weight="bold"> | ||
{regexMessage} | ||
</PasswordMessage> | ||
) : undefined} | ||
</PasswordField> | ||
<PasswordField direction="column"> | ||
<Typography size="body" weight="bold"> | ||
{t("New password (for confirmation)")} | ||
</Typography> | ||
<TextInput | ||
value={passwordConfirmation} | ||
onChange={setPasswordConfirmation} | ||
type="password" | ||
/> | ||
{isMatchPassword ? ( | ||
<PasswordMessage size="body" weight="regular" color="red"> | ||
<span> | ||
<Icon icon="warning" size="large" color="red" /> | ||
</span> | ||
"repeatPassword" Passwords need to match. | ||
</PasswordMessage> | ||
) : undefined} | ||
</PasswordField> | ||
</div> | ||
</ModalContentWrapper> | ||
</ModalPanel> | ||
</Modal> | ||
); | ||
}; | ||
|
||
const ModalContentWrapper = styled("div")(({ theme }) => ({ | ||
display: "flex", | ||
flexDirection: "column", | ||
gap: theme.spacing.large, | ||
padding: theme.spacing.large, | ||
background: theme.bg[1] | ||
})); | ||
|
||
const SubText = styled.div` | ||
margin: ${({ theme }) => `${theme.spacing.large}px auto`}; | ||
`; | ||
|
||
const PasswordField = styled(Flex)` | ||
height: 50px; | ||
transition: all 0.2s; | ||
&:has(p ~ p) { | ||
height: 68px; | ||
} | ||
`; | ||
|
||
const PasswordMessage = styled(Typography)` | ||
margin-top: ${metricsSizes["s"]}px; | ||
font-style: italic; | ||
display: flex; | ||
`; | ||
|
||
export default PasswordModal; |
Oops, something went wrong.