diff --git a/web/src/beta/features/AccountAndWorkSpaceSetting/hooks.ts b/web/src/beta/features/AccountAndWorkSpaceSetting/hooks.ts new file mode 100644 index 000000000..6312bf943 --- /dev/null +++ b/web/src/beta/features/AccountAndWorkSpaceSetting/hooks.ts @@ -0,0 +1,42 @@ +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) => { + try { + await useUpdatePassword({ password, passwordConfirmation }); + } catch (error) { + console.error("Failed to update password:", error); + } + }, + [useUpdatePassword] + ); + + const handleDeleteUser = useCallback(async () => { + try { + const userId = data.id; + if (userId) await useDeleteUser({ userId }); + } catch (error) { + console.error("Failed to delete user:", error); + } + }, [data.id, useDeleteUser]); + + return { + meData: data, + passwordPolicy, + handleUpdateUserPassword, + handleDeleteUser + }; +}; diff --git a/web/src/beta/features/AccountAndWorkSpaceSetting/index.tsx b/web/src/beta/features/AccountAndWorkSpaceSetting/index.tsx new file mode 100644 index 000000000..e4a593205 --- /dev/null +++ b/web/src/beta/features/AccountAndWorkSpaceSetting/index.tsx @@ -0,0 +1,132 @@ +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 = ({ 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(); + const { name, email } = meData; + + return ( + + + + + + + {tabs?.map((t) => ( + + ))} + + + + + + {tab === "account" && ( + + )} + + + + ); +}; + +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: theme.radius.large + }, + ["* ::-webkit-scrollbar-thumb"]: { + background: theme.relative.light, + borderRadius: theme.radius.small + }, + ["* ::-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; diff --git a/web/src/beta/features/AccountAndWorkSpaceSetting/innerPages/AccountSetting/PasswordModal/index.tsx b/web/src/beta/features/AccountAndWorkSpaceSetting/innerPages/AccountSetting/PasswordModal/index.tsx new file mode 100644 index 000000000..735f5b02c --- /dev/null +++ b/web/src/beta/features/AccountAndWorkSpaceSetting/innerPages/AccountSetting/PasswordModal/index.tsx @@ -0,0 +1,208 @@ +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, useTheme } 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 = { + isVisible: boolean; + onClose?: () => void; + passwordPolicy?: PasswordPolicy; + onPasswordUpdate?: ({ + password, + passwordConfirmation + }: { + password: string; + passwordConfirmation: string; + }) => void; +}; + +const PasswordModal: React.FC = ({ + isVisible, + onClose, + passwordPolicy, + onPasswordUpdate +}) => { + const t = useT(); + const theme = useTheme(); + + const [password, setPassword] = useState(""); + const [regexMessage, setRegexMessage] = useState(); + const [regexMessageColor, setRegexMessageColor] = useState< + string | undefined + >(); + const [passwordConfirmation, setPasswordConfirmation] = useState(); + 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.")); + setRegexMessageColor(theme.warning.main); + break; + case passwordPolicy?.tooShort?.test(password ?? ""): + setRegexMessage(t("Too short.")); + setRegexMessageColor(theme.warning.main); + break; + case passwordPolicy?.tooLong?.test(password ?? ""): + setRegexMessage(t("That is terribly long.")); + setRegexMessageColor(theme.warning.main); + break; + case passwordPolicy?.highSecurity?.test(password ?? ""): + setRegexMessage(t("That password is great!")); + setRegexMessageColor(theme.primary.main); + break; + case passwordPolicy?.medSecurity?.test(password ?? ""): + setRegexMessage(t("That password need more security.")); + setRegexMessageColor(theme.warning.main); + break; + case passwordPolicy?.lowSecurity?.test(password ?? ""): + setRegexMessage(t("That password is low security.")); + setRegexMessageColor(theme.dangerous.main); + break; + default: + setRegexMessage(t("That password confuses me, but might be okay.")); + break; + } + }, + [t, passwordPolicy, theme] + ); + + const handleClose = useCallback(() => { + setPassword(""); + setPasswordConfirmation(""); + onClose?.(); + }, [onClose]); + + const handleSave = useCallback(() => { + if (password === passwordConfirmation) { + onPasswordUpdate?.({ password, passwordConfirmation }); + handleClose(); + } + }, [onPasswordUpdate, 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 ( + + + ]} + > + + + {t( + `In order to protect your account, make sure your password is unique and strong.` + )} + + + {t("New password")} + + {password ? ( + + {regexMessage} + + ) : undefined} + + + + {t("New password (for confirmation)")} + + + {isMatchPassword ? ( + + + {t('"repeatPassword" Passwords need to match')} + + ) : undefined} + + + + + ); +}; + +const ModalContentWrapper = styled("div")(({ theme }) => ({ + display: "flex", + flexDirection: "column", + gap: theme.spacing.large, + padding: theme.spacing.large, + background: theme.bg[1] +})); + +const PasswordField = styled(Flex)(({ theme }) => ({ + height: "50px", + transition: "all 0.2s", + "& > *:first-child": { + marginBottom: theme.spacing.small + }, + "&:has(p ~ p)": { + height: "68px" + } +})); + +const PasswordMessage = styled(Typography)` + margin-top: ${metricsSizes["s"]}px; + display: flex; +`; + +export default PasswordModal; diff --git a/web/src/beta/features/AccountAndWorkSpaceSetting/innerPages/AccountSetting/index.tsx b/web/src/beta/features/AccountAndWorkSpaceSetting/innerPages/AccountSetting/index.tsx new file mode 100644 index 000000000..9383b5657 --- /dev/null +++ b/web/src/beta/features/AccountAndWorkSpaceSetting/innerPages/AccountSetting/index.tsx @@ -0,0 +1,99 @@ +import { + Collapse, + TextInput, + Typography, + IconButton +} from "@reearth/beta/lib/reearth-ui"; +import { InputField } from "@reearth/beta/ui/fields"; +import { PasswordPolicy } from "@reearth/services/config/passwordPolicy"; +import { useT } from "@reearth/services/i18n"; +import { styled } from "@reearth/services/theme"; +import { FC, useState } from "react"; + +import { UpdatePasswordType } from "../../hooks"; +import { InnerPage, SettingsWrapper, SettingsFields } from "../common"; + +import PasswordModal from "./PasswordModal"; + +type Props = { + informationData: { name?: string; email?: string }; + passwordPolicy?: PasswordPolicy; + onUpdateUserPassword: ({ + password, + passwordConfirmation + }: UpdatePasswordType) => Promise; +}; + +const AccountSetting: FC = ({ + passwordPolicy, + onUpdateUserPassword, + informationData +}) => { + const t = useT(); + const [changePasswordModal, setChangePasswordModal] = + useState(false); + + return ( + + + + + + + + + {t("Password")} + + + { + setChangePasswordModal(true); + }} + size="medium" + hasBorder={true} + /> + + + + + + + setChangePasswordModal(false)} + onPasswordUpdate={onUpdateUserPassword} + /> + + ); +}; +export default AccountSetting; + +const PasswordWrapper = styled("div")(() => ({ + display: "flex", + flexDirection: "column", + width: "100%" +})); + +const PasswordInputWrapper = styled("div")(({ theme }) => ({ + display: "flex", + gap: theme.spacing.smallest, + alignItems: "center" +})); diff --git a/web/src/beta/features/AccountAndWorkSpaceSetting/innerPages/common.tsx b/web/src/beta/features/AccountAndWorkSpaceSetting/innerPages/common.tsx new file mode 100644 index 000000000..4a4ac5fc8 --- /dev/null +++ b/web/src/beta/features/AccountAndWorkSpaceSetting/innerPages/common.tsx @@ -0,0 +1,54 @@ +import { styled } from "@reearth/services/theme"; + +export const InnerPage = styled("div")<{ + wide?: boolean; + transparent?: boolean; +}>(({ wide, transparent, theme }) => ({ + boxSizing: "border-box", + display: "flex", + width: "100%", + maxWidth: wide ? 950 : 750, + backgroundColor: transparent ? "none" : theme.bg[1], + borderRadius: theme.radius.normal +})); + +export const InnerSidebar = styled("div")(({ theme }) => ({ + display: "flex", + flexDirection: "column", + width: 213, + borderRight: `1px solid ${theme.outline.weaker}`, + padding: `${theme.spacing.normal}px 0` +})); + +export const SettingsWrapper = styled("div")(({ theme }) => ({ + display: "flex", + flexDirection: "column", + width: "100%", + flex: 1, + ["> div:not(:last-child)"]: { + borderBottom: `1px solid ${theme.outline.weaker}` + } +})); + +export const SettingsFields = styled("div")(({ theme }) => ({ + display: "flex", + flexDirection: "column", + gap: theme.spacing.largest +})); + +export const SettingsRow = styled("div")(({ theme }) => ({ + display: "flex", + justifyContent: "space-between", + flexDirection: "row", + gap: theme.spacing.largest +})); + +export const SettingsRowItem = styled("div")(() => ({ + width: "100%" +})); + +export const ButtonWrapper = styled("div")(({ theme }) => ({ + display: "flex", + justifyContent: "flex-end", + gap: theme.spacing.small +})); diff --git a/web/src/beta/features/Dashboard/LeftSidePanel/profile.tsx b/web/src/beta/features/Dashboard/LeftSidePanel/profile.tsx index d79e87c96..680b71089 100644 --- a/web/src/beta/features/Dashboard/LeftSidePanel/profile.tsx +++ b/web/src/beta/features/Dashboard/LeftSidePanel/profile.tsx @@ -8,6 +8,7 @@ import { useT } from "@reearth/services/i18n"; import { styled, useTheme } from "@reearth/services/theme"; import { ProjectType } from "@reearth/types"; import { FC } from "react"; +import { useNavigate } from "react-router-dom"; import { Workspace } from "../type"; @@ -37,6 +38,7 @@ export const Profile: FC = ({ }) => { const t = useT(); const theme = useTheme(); + const navigate = useNavigate(); const popupMenu: PopupMenuItem[] = [ { @@ -56,6 +58,12 @@ export const Profile: FC = ({ }; }) }, + { + id: "accountSettings", + title: t("Account Settings"), + icon: "user", + onClick: () => navigate("/settings/account") + }, { id: "signOut", title: t("Log Out"), diff --git a/web/src/beta/features/Navbar/LeftSection/index.tsx b/web/src/beta/features/Navbar/LeftSection/index.tsx index 5b8339501..fddfd3522 100644 --- a/web/src/beta/features/Navbar/LeftSection/index.tsx +++ b/web/src/beta/features/Navbar/LeftSection/index.tsx @@ -1,10 +1,11 @@ import { IconButton, PopupMenu, - PopupMenuItem + PopupMenuItem, + Typography } from "@reearth/beta/lib/reearth-ui"; import { useT } from "@reearth/services/i18n"; -import { styled } from "@reearth/services/theme"; +import { styled, useTheme } from "@reearth/services/theme"; import { useMemo } from "react"; import { Link } from "react-router-dom"; @@ -32,6 +33,7 @@ const LeftSection: React.FC = ({ onWorkspaceChange }) => { const t = useT(); + const theme = useTheme(); const menuItems: PopupMenuItem[] = useMemo( () => [ @@ -55,6 +57,13 @@ const LeftSection: React.FC = ({ return ( + {page !== "editor" && ( + + + {t("Visualizer")} + + + )} = ({ iconRotate, iconColor, stopPropagationOnClick, - onClick + onClick, + hasBorder }) => { const handleClick = useCallback( (e: MouseEvent) => { @@ -46,6 +48,7 @@ export const IconButton: FC = ({ active={active} iconRotate={iconRotate} onClick={handleClick} + hasBorder={hasBorder} > @@ -53,32 +56,38 @@ export const IconButton: FC = ({ }; const StyledButton = styled("button")<{ - size: "normal" | "small" | "smallest" | "large"; + size: "normal" | "small" | "smallest" | "medium" | "large"; appearance: "primary" | "secondary" | "dangerous" | "simple"; active?: boolean; iconRotate?: string; -}>(({ appearance, size, active, iconRotate, theme }) => ({ + hasBorder?: boolean; +}>(({ appearance, size, active, iconRotate, theme, hasBorder }) => ({ display: "flex", flexDirection: "row", alignItems: "center", justifyContent: "center", + border: hasBorder ? `1px solid ${theme.outline.weak}` : "none", flexShrink: 0, width: size === "smallest" ? "16px" : size === "small" ? "20px" - : size === "large" - ? "36px" - : "24px", + : size === "medium" + ? "28px" + : size === "large" + ? "36px" + : "24px", height: size === "smallest" ? "16px" : size === "small" ? "20px" - : size === "large" - ? "36px" - : "24px", + : size === "medium" + ? "28px" + : size === "large" + ? "36px" + : "24px", borderRadius: size === "small" ? `${theme.radius.small}px` : `${theme.radius.normal}px`, color: active diff --git a/web/src/beta/lib/reearth-ui/components/TextInput/index.tsx b/web/src/beta/lib/reearth-ui/components/TextInput/index.tsx index 383ea58c4..63bc29965 100644 --- a/web/src/beta/lib/reearth-ui/components/TextInput/index.tsx +++ b/web/src/beta/lib/reearth-ui/components/TextInput/index.tsx @@ -23,6 +23,7 @@ export type TextInputProps = { onChange?: (text: string) => void; onBlur?: (text: string) => void; onKeyDown?: (e: KeyboardEvent) => void; + type?: string; }; export const TextInput: FC = ({ @@ -38,7 +39,8 @@ export const TextInput: FC = ({ autoFocus, onChange, onBlur, - onKeyDown + onKeyDown, + type }) => { const [currentValue, setCurrentValue] = useState(value ?? ""); const [isFocused, setIsFocused] = useState(false); @@ -90,6 +92,7 @@ export const TextInput: FC = ({ appearance={appearance} autoFocus={autoFocus} onKeyDown={onKeyDown} + type={type} /> {actions && {actions}} diff --git a/web/src/beta/pages/AccountSettingsPage/index.tsx b/web/src/beta/pages/AccountSettingsPage/index.tsx index 330e41b85..800aa666d 100644 --- a/web/src/beta/pages/AccountSettingsPage/index.tsx +++ b/web/src/beta/pages/AccountSettingsPage/index.tsx @@ -1,12 +1,14 @@ -import { Typography } from "@reearth/beta/lib/reearth-ui"; +import AccountAndWorkSpaceSetting from "@reearth/beta/features/AccountAndWorkSpaceSetting"; import { FC } from "react"; -export type Props = { - path?: string; -}; +import Page from "../Page"; -const AccountPage: FC = () => ( - Account page +const AccountSettingPage: FC = () => ( + ( + + )} + /> ); -export default AccountPage; +export default AccountSettingPage; diff --git a/web/src/services/api/meApi.ts b/web/src/services/api/meApi.ts index 0a84a07b9..e1ecaf6b2 100644 --- a/web/src/services/api/meApi.ts +++ b/web/src/services/api/meApi.ts @@ -1,8 +1,18 @@ -import { useQuery } from "@apollo/client"; -import { GET_ME } from "@reearth/services/gql/queries/user"; +import { useMutation, useQuery } from "@apollo/client"; +import { + GET_ME, + DELETE_ME, + UPDATE_ME +} from "@reearth/services/gql/queries/user"; import { useCallback } from "react"; +import { useT } from "../i18n"; +import { useNotification } from "../state"; + export default () => { + const t = useT(); + const [, setNotification] = useNotification(); + const useMeQuery = useCallback((options?: { skip?: boolean }) => { const { data, ...rest } = useQuery(GET_ME, { ...options }); return { @@ -11,7 +21,65 @@ export default () => { }; }, []); + const [updateMeMutation] = useMutation(UPDATE_ME); + const useUpdatePassword = useCallback( + async ({ + password, + passwordConfirmation + }: { + password: string; + passwordConfirmation: string; + }) => { + const { data, errors } = await updateMeMutation({ + variables: { + password, + passwordConfirmation + } + }); + + if (errors || !data?.updateMe) { + console.log("GraphQL: Failed to update password", errors); + setNotification({ + type: "error", + text: t("Failed to update user password.") + }); + return { status: "error" }; + } + setNotification({ + type: "success", + text: t("Successfully updated user password!") + }); + return { data: data?.updateMe, status: "success" }; + }, + [setNotification, t, updateMeMutation] + ); + + const [deleteMeMutation] = useMutation(DELETE_ME); + const useDeleteUser = useCallback( + async ({ userId }: { userId: string }) => { + const { data, errors } = await deleteMeMutation({ + variables: { userId } + }); + if (errors || !data?.deleteMe) { + console.log("GraphQL: Failed to delete users", errors); + setNotification({ + type: "error", + text: t("Failed to delete user.") + }); + return { status: "error" }; + } + setNotification({ + type: "success", + text: t("Successfully delete user!") + }); + return { data: data.deleteMe, status: "success" }; + }, + [deleteMeMutation, setNotification, t] + ); + return { - useMeQuery + useMeQuery, + useUpdatePassword, + useDeleteUser }; }; diff --git a/web/src/services/i18n/translations/en.yml b/web/src/services/i18n/translations/en.yml index 23f1eeedc..732b2416e 100644 --- a/web/src/services/i18n/translations/en.yml +++ b/web/src/services/i18n/translations/en.yml @@ -1,6 +1,22 @@ You have passed the maximum value.: '' You have passed the minimum value.: '' Not found: '' +Account: '' +Name: '' +Email address: '' +Password: '' +No whitespace is allowed.: '' +Too short.: '' +That is terribly long.: '' +That password is great!: '' +That password need more security.: '' +That password is low security.: '' +That password confuses me, but might be okay.: '' +Change password: '' +In order to protect your account, make sure your password is unique and strong.: '' +New password: '' +New password (for confirmation): '' +'"repeatPassword" Passwords need to match': '' Select Asset: '' Last Uploaded: '' First Uploaded: '' @@ -12,7 +28,6 @@ Upload File: '' Search in all assets library: '' Assets selected: '' Asset selected: '' -Name: '' Uploaded At: '' Size: '' No Asset has been uploaded yet: '' @@ -43,6 +58,7 @@ Starred: '' Switch WorkSpace: '' Personal: '' Team Workspace: '' +Account Settings: '' Log Out: '' Re:Earth Visualizer: '' Page: '' @@ -174,6 +190,7 @@ Align System: '' Widget Manager: '' Project settings: '' Plugin: '' +Visualizer: '' Unknown: '' Switch workspace: '' Log out: '' @@ -216,7 +233,6 @@ Public Info: '' Basic Authorization: '' Enable Basic Authorization: '' Username: '' -Password: '' Site Setting: '' Site name: '' You are about to change the site name for your project. Only alphanumeric characters and hyphens are allows.: '' @@ -335,6 +351,10 @@ Failed to update the layerStyle.: '' Successfully updated a the layerStyle!: '' Failed to delete the layer style.: '' Successfully deleted the layer style!: '' +Failed to update user password.: '' +Successfully updated user password!: '' +Failed to delete user.: '' +Successfully delete user!: '' Failed to install plugin.: '' Successfully installed plugin!: '' Failed to upgrade plugin.: '' diff --git a/web/src/services/i18n/translations/ja.yml b/web/src/services/i18n/translations/ja.yml index 3bb8d74a2..07daf70bc 100644 --- a/web/src/services/i18n/translations/ja.yml +++ b/web/src/services/i18n/translations/ja.yml @@ -1,6 +1,22 @@ You have passed the maximum value.: '' You have passed the minimum value.: '' Not found: ページが見つかりません +Account: '' +Name: 名前 +Email address: '' +Password: パスワード +No whitespace is allowed.: '' +Too short.: '' +That is terribly long.: '' +That password is great!: '' +That password need more security.: '' +That password is low security.: '' +That password confuses me, but might be okay.: '' +Change password: '' +In order to protect your account, make sure your password is unique and strong.: '' +New password: '' +New password (for confirmation): '' +'"repeatPassword" Passwords need to match': '' Select Asset: '' Last Uploaded: 昇順(アップロード日) First Uploaded: 降順(アップロード日) @@ -12,7 +28,6 @@ Upload File: '' Search in all assets library: '' Assets selected: '' Asset selected: '' -Name: 名前 Uploaded At: '' Size: '' No Asset has been uploaded yet: '' @@ -43,6 +58,7 @@ Starred: '' Switch WorkSpace: '' Personal: '' Team Workspace: '' +Account Settings: '' Log Out: '' Re:Earth Visualizer: '' Page: ページ @@ -174,6 +190,7 @@ Align System: アラインシステム Widget Manager: ウィジェット管理 Project settings: プロジェクト設定 Plugin: プラグイン +Visualizer: '' Unknown: 不明 Switch workspace: '' Log out: ログアウト @@ -216,7 +233,6 @@ Public Info: 公開詳細設定 Basic Authorization: ベーシック認証 Enable Basic Authorization: ベーシック認証を有効化 Username: ユーザー名 -Password: パスワード Site Setting: サイト設定 Site name: サイト名 You are about to change the site name for your project. Only alphanumeric characters and hyphens are allows.: プロジェクト公開ページのサイト名を変更します。アルファベットとハイフン(-)のみ利用可能です。 @@ -335,6 +351,10 @@ Failed to update the layerStyle.: レイヤースタイルのアップデート Successfully updated a the layerStyle!: レイヤースタイルのアップデートに成功しました! Failed to delete the layer style.: '' Successfully deleted the layer style!: '' +Failed to update user password.: '' +Successfully updated user password!: '' +Failed to delete user.: '' +Successfully delete user!: '' Failed to install plugin.: プラグインのインストールに失敗しました。 Successfully installed plugin!: プラグインがインストールされました。 Failed to upgrade plugin.: プラグインのアップグレードに失敗しました。 diff --git a/web/src/services/routing/index.tsx b/web/src/services/routing/index.tsx index 3061903a3..4f2661bf0 100644 --- a/web/src/services/routing/index.tsx +++ b/web/src/services/routing/index.tsx @@ -1,3 +1,4 @@ +import AccountSettingPage from "@reearth/beta/pages/AccountSettingsPage"; import RootPage from "@reearth/beta/pages/RootPage"; import { styled } from "@reearth/services/theme"; import { lazy } from "react"; @@ -34,6 +35,10 @@ export const AppRoutes = () => { path: "settings/project/:projectId/:tab?/:subId?", element: }, + { + path: "settings/account", + element: + }, { path: "graphql", element: