Skip to content

Commit

Permalink
feat: add account page to change password
Browse files Browse the repository at this point in the history
  • Loading branch information
tcsola authored and tcsola committed Sep 26, 2024
1 parent d428b46 commit 896ae1d
Show file tree
Hide file tree
Showing 12 changed files with 690 additions and 23 deletions.
34 changes: 34 additions & 0 deletions web/src/beta/features/AccountAndWorkSpaceSetting/hooks.ts
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 web/src/beta/features/AccountAndWorkSpaceSetting/index.tsx
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;
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;
Loading

0 comments on commit 896ae1d

Please sign in to comment.