diff --git a/dictation_client/src/App.tsx b/dictation_client/src/App.tsx index 122ecc1..01e8577 100644 --- a/dictation_client/src/App.tsx +++ b/dictation_client/src/App.tsx @@ -4,11 +4,14 @@ import { PublicClientApplication } from "@azure/msal-browser"; import { MsalProvider, useMsal } from "@azure/msal-react"; import { msalConfig } from "common/msalConfig"; import { useEffect, useLayoutEffect } from "react"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import globalAxios, { AxiosError, AxiosResponse } from "axios"; import { clearToken } from "features/auth"; import { useTranslation } from "react-i18next"; import "./styles/GlobalStyle.css"; +import Snackbar from "components/snackbar"; +import { selectSnackber } from "features/ui/selectors"; +import { closeSnackbar } from "features/ui/uiSlice"; const App = (): JSX.Element => { const dispatch = useDispatch(); @@ -48,12 +51,25 @@ const App = (): JSX.Element => { } }, [i18n]); + const snackbarInfo = useSelector(selectSnackber); + return ( - - - - - + <> + { + dispatch(closeSnackbar()); + }} + /> + + + + + + ); }; diff --git a/dictation_client/src/app/store.ts b/dictation_client/src/app/store.ts index 190d18c..e500f65 100644 --- a/dictation_client/src/app/store.ts +++ b/dictation_client/src/app/store.ts @@ -3,6 +3,7 @@ import login from "features/login/loginSlice"; import auth from "features/auth/authSlice"; import signup from "features/signup/signupSlice"; import verify from "features/verify/verifySlice"; +import ui from "features/ui/uiSlice"; export const store = configureStore({ reducer: { @@ -10,6 +11,7 @@ export const store = configureStore({ auth, signup, verify, + ui, }, }); diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index bb24063..1e05efe 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -21,5 +21,6 @@ export const errorCodes = [ "E000105", // トークン発行元エラー "E000106", // トークンアルゴリズムエラー "E010201", // 未認証ユーザエラー + "E010202", // 認証済ユーザエラー "E010301", // メールアドレス登録済みエラー ] as const; diff --git a/dictation_client/src/components/snackbar/index.tsx b/dictation_client/src/components/snackbar/index.tsx new file mode 100644 index 0000000..27b80b4 --- /dev/null +++ b/dictation_client/src/components/snackbar/index.tsx @@ -0,0 +1,71 @@ +import React, { useCallback, useEffect, useRef } from "react"; +import reportWhite from "../../assets/images/report_white.svg"; +import closeWhite from "../../assets/images/close_white.svg"; +import checkCircleWhite from "../../assets/images/check_circle_white.svg"; +import styles from "../../styles/app.module.scss"; + +export type SnackbarLevel = "info" | "error"; + +interface SnackbarProps { + isOpen: boolean; + level: SnackbarLevel; + message: string; + duration?: number; + onClose: () => void; +} + +const Snackbar: React.FC = (props) => { + const { isOpen, level, message, duration, onClose } = props; + + const timer = useRef(0); + const onTimeout = useCallback(() => { + timer.current = window.setTimeout(() => onClose(), duration); + }, [onClose, duration]); + + const onCloseSnackbar = useCallback(() => { + clearTimeout(timer.current); + onClose(); + }, [onClose]); + + useEffect(() => { + if (duration !== 0) { + onTimeout(); + } + return () => { + clearTimeout(timer.current); + }; + }, [duration, onTimeout]); + + const isAlert = level === "error" ? styles.isAlert : ""; + const isShow = isOpen ? styles.isShow : ""; + + return ( +
+ {level === "error" ? ( + check + ) : ( + report + )} +

{message}

+ {level === "error" && ( + + )} +
+ ); +}; + +Snackbar.defaultProps = { + duration: 0, +}; + +export default Snackbar; diff --git a/dictation_client/src/features/signup/operations.ts b/dictation_client/src/features/signup/operations.ts index 109861b..dce701a 100644 --- a/dictation_client/src/features/signup/operations.ts +++ b/dictation_client/src/features/signup/operations.ts @@ -1,5 +1,8 @@ import { createAsyncThunk } from "@reduxjs/toolkit"; import type { RootState } from "app/store"; +import { ErrorObject, createErrorObject } from "common/errors"; +import { getTranslationID } from "translation"; +import { closeSnackbar, openSnackbar } from "features/ui/uiSlice"; import { AccountsApi, CreateAccountRequest } from "../../api/api"; import { Configuration } from "../../api/configuration"; @@ -11,7 +14,7 @@ export const signupAsync = createAsyncThunk< { // rejectした時の返却値の型 rejectValue: { - /* Empty Object */ + error: ErrorObject; }; } >("login/signupAsync", async (args, thunkApi) => { @@ -24,10 +27,32 @@ export const signupAsync = createAsyncThunk< const accountApi = new AccountsApi(config); try { - const { data } = await accountApi.createAccount(createAccountRequest); + thunkApi.dispatch(closeSnackbar()); + await accountApi.createAccount(createAccountRequest); + + thunkApi.dispatch(closeSnackbar()); return {}; } catch (e) { - return thunkApi.rejectWithValue({}); + const error = createErrorObject(e); + if (error.code === "E010301") { + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID( + "signupConfirmPage.message.emailConflictError" + ), + }) + ); + } else { + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + } + + return thunkApi.rejectWithValue({ error }); } }); diff --git a/dictation_client/src/features/signup/signupSlice.ts b/dictation_client/src/features/signup/signupSlice.ts index 390a245..d75f385 100644 --- a/dictation_client/src/features/signup/signupSlice.ts +++ b/dictation_client/src/features/signup/signupSlice.ts @@ -51,13 +51,13 @@ export const signupSlice = createSlice({ }, }, extraReducers: (builder) => { - builder.addCase(signupAsync.pending, (state) => { + builder.addCase(signupAsync.pending, () => { // }); builder.addCase(signupAsync.fulfilled, (state) => { state.apps.pageState = "complete"; }); - builder.addCase(signupAsync.rejected, (state) => { + builder.addCase(signupAsync.rejected, () => { // }); }, diff --git a/dictation_client/src/features/ui/constants.ts b/dictation_client/src/features/ui/constants.ts new file mode 100644 index 0000000..9374dea --- /dev/null +++ b/dictation_client/src/features/ui/constants.ts @@ -0,0 +1,2 @@ +// 標準のスナックバー表示時間(ミリ秒) +export const DEFAULT_SNACKBAR_DURATION = 3000; diff --git a/dictation_client/src/features/ui/index.ts b/dictation_client/src/features/ui/index.ts new file mode 100644 index 0000000..e1354d4 --- /dev/null +++ b/dictation_client/src/features/ui/index.ts @@ -0,0 +1,4 @@ +export * from "./constants"; +export * from "./selectors"; +export * from "./state"; +export * from "./uiSlice"; diff --git a/dictation_client/src/features/ui/selectors.ts b/dictation_client/src/features/ui/selectors.ts new file mode 100644 index 0000000..4cb1543 --- /dev/null +++ b/dictation_client/src/features/ui/selectors.ts @@ -0,0 +1,15 @@ +import { RootState } from "app/store"; +import { SnackbarLevel } from "components/snackbar"; + +export const selectSnackber = ( + state: RootState +): { + isOpen: boolean; + level: SnackbarLevel; + message: string; + duration?: number; +} => { + const { isOpen, level, message, duration } = state.ui; + + return { isOpen, level, message, duration }; +}; diff --git a/dictation_client/src/features/ui/state.ts b/dictation_client/src/features/ui/state.ts new file mode 100644 index 0000000..ade4c09 --- /dev/null +++ b/dictation_client/src/features/ui/state.ts @@ -0,0 +1,8 @@ +import { SnackbarLevel } from "components/snackbar"; + +export interface UIState { + isOpen: boolean; + level: SnackbarLevel; + message: string; + duration?: number; +} diff --git a/dictation_client/src/features/ui/uiSlice.ts b/dictation_client/src/features/ui/uiSlice.ts new file mode 100644 index 0000000..bf82331 --- /dev/null +++ b/dictation_client/src/features/ui/uiSlice.ts @@ -0,0 +1,38 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { SnackbarLevel } from "components/snackbar"; +import { UIState } from "./state"; +import { DEFAULT_SNACKBAR_DURATION } from "./constants"; + +const initialState: UIState = { + isOpen: false, + level: "error", + message: "", +}; + +export const uiSlice = createSlice({ + name: "ui", + initialState, + reducers: { + openSnackbar: ( + state, + action: PayloadAction<{ + level: SnackbarLevel; + message: string; + duration?: number; + }> + ) => { + const { level, message, duration } = action.payload; + state.isOpen = true; + state.level = level; + state.message = message; + state.duration = + level === "error" ? undefined : duration ?? DEFAULT_SNACKBAR_DURATION; + }, + closeSnackbar: (state) => { + state.isOpen = false; + }, + }, +}); +export const { openSnackbar, closeSnackbar } = uiSlice.actions; + +export default uiSlice.reducer; diff --git a/dictation_client/src/features/verify/verifySlice.ts b/dictation_client/src/features/verify/verifySlice.ts index 6dfe7af..1039f64 100644 --- a/dictation_client/src/features/verify/verifySlice.ts +++ b/dictation_client/src/features/verify/verifySlice.ts @@ -23,7 +23,7 @@ export const verifySlice = createSlice({ const { payload } = action; // メール認証済みかをエラーコードから判定 - if (payload?.error.code === "E010301") { + if (payload?.error.code === "E010202") { state.apps.VerifyState = "alreadySuccess"; } else { state.apps.VerifyState = "failed"; diff --git a/dictation_client/src/pages/SignupCompletePage/index.tsx b/dictation_client/src/pages/SignupCompletePage/index.tsx index c9bd65e..50822c5 100644 --- a/dictation_client/src/pages/SignupCompletePage/index.tsx +++ b/dictation_client/src/pages/SignupCompletePage/index.tsx @@ -1,4 +1,3 @@ -import { AppDispatch } from "app/store"; import React from "react"; import { useTranslation } from "react-i18next"; import { getTranslationID } from "translation"; diff --git a/dictation_client/src/pages/SignupPage/signupInput.tsx b/dictation_client/src/pages/SignupPage/signupInput.tsx index 59a2203..bd3bfca 100644 --- a/dictation_client/src/pages/SignupPage/signupInput.tsx +++ b/dictation_client/src/pages/SignupPage/signupInput.tsx @@ -105,8 +105,9 @@ const SignupInput: React.FC = (): JSX.Element => { /> {isPushCreateButton && hasErrorEmptyCompany && ( - {" "} - {t(getTranslationID("common.message.inputEmptyError"))} + {t( + getTranslationID("signupPage.message.inputEmptyError") + )} )} @@ -132,7 +133,11 @@ const SignupInput: React.FC = (): JSX.Element => { ))} {isPushCreateButton && hasErrorEmptyCountry && ( - Error message + + {t( + getTranslationID("signupPage.message.inputEmptyError") + )} + )} {t(getTranslationID("signupPage.text.countryExplanation"))} @@ -170,7 +175,9 @@ const SignupInput: React.FC = (): JSX.Element => { {isPushCreateButton && hasErrorEmptyAdminName && ( {" "} - {t(getTranslationID("common.message.inputEmptyError"))} + {t( + getTranslationID("signupPage.message.inputEmptyError") + )} )} @@ -193,13 +200,17 @@ const SignupInput: React.FC = (): JSX.Element => { /> {isPushCreateButton && hasErrorEmptyEmail && ( - {t(getTranslationID("common.message.inputEmptyError"))} + {t( + getTranslationID("signupPage.message.inputEmptyError") + )} )} {isPushCreateButton && hasErrorIncorrectEmail && ( {t( - getTranslationID("common.message.emailIncorrectError") + getTranslationID( + "signupPage.message.emailIncorrectError" + ) )} )} @@ -234,14 +245,16 @@ const SignupInput: React.FC = (): JSX.Element => { /> {isPushCreateButton && hasErrorEmptyPassword && ( - {t(getTranslationID("common.message.inputEmptyError"))} + {t( + getTranslationID("signupPage.message.inputEmptyError") + )} )} {isPushCreateButton && hasErrorIncorrectPassword && ( {t( getTranslationID( - "common.message.passwordIncorrectError" + "signupPage.message.passwordIncorrectError" ) )} diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 6129561..9feb5ce 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -3,7 +3,8 @@ "message": { "inputEmptyError": "(de)Error Message", "passwordIncorrectError": "(de)Error Message", - "emailIncorrectError": "(de)Error Message" + "emailIncorrectError": "(de)Error Message", + "internalServerError": "(de)処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。" }, "label": { "cancel": "(de)Cancel", @@ -25,6 +26,11 @@ } }, "signupPage": { + "message": { + "inputEmptyError": "(de)この項目の入力は必須です。入力してください。", + "passwordIncorrectError": "(de)入力されたパスワードがルールを満たしていません。下記のルールを満たすパスワードを入力してください。", + "emailIncorrectError": "(de)メールアドレスの形式が不正です。正しいメールアドレスの形式で入力してください。" + }, "text": { "title": "(de)Create your account", "pageExplanation": "(de)Explanation...", @@ -48,12 +54,14 @@ } }, "signupConfirmPage": { + "message": { + "emailConflictError": "(de)このメールアドレスは既に登録されています。他のメールアドレスで登録してください。" + }, "text": { "title": "(de)Confirmation", "pageExplanation": "(de)Explanation ......", "accountInfoTitle": "(de)Your account information", - "adminInfoTitle": "(de)Primary administrator's information", - "createdInfo": "(de)Your account has been created and a verification email has been set to your registered email address. Please click on the verification link included in the email to activate your account." + "adminInfoTitle": "(de)Primary administrator's information" }, "label": { "company": "(de)Company Name", @@ -62,12 +70,15 @@ "adminName": "(de)Admin Name", "email": "(de)Email", "password": "(de)Password", - "title": "(de)Account created" + "signupButton": "(de)Sign up" } }, "signupCompletePage": { + "text": { + "createdInfo": "(de)Your account has been created and a verification email has been set to your registered email address. Please click on the verification link included in the email to activate your account." + }, "label": { - "signupButton": "(de)Sign up" + "title": "(de)Account created" } }, "signupVerifyPage": { @@ -83,4 +94,4 @@ "returnToSignIn": "(de)Return to Sign in" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 87c409f..9640421 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -3,7 +3,8 @@ "message": { "inputEmptyError": "Error Message", "passwordIncorrectError": "Error Message", - "emailIncorrectError": "Error Message" + "emailIncorrectError": "Error Message", + "internalServerError": "処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。" }, "label": { "cancel": "Cancel", @@ -25,6 +26,11 @@ } }, "signupPage": { + "message": { + "inputEmptyError": "この項目の入力は必須です。入力してください。", + "passwordIncorrectError": "入力されたパスワードがルールを満たしていません。下記のルールを満たすパスワードを入力してください。", + "emailIncorrectError": "メールアドレスの形式が不正です。正しいメールアドレスの形式で入力してください。" + }, "text": { "title": "Create your account", "pageExplanation": "Explanation...", @@ -48,12 +54,14 @@ } }, "signupConfirmPage": { + "message": { + "emailConflictError": "このメールアドレスは既に登録されています。他のメールアドレスで登録してください。" + }, "text": { "title": "Confirmation", "pageExplanation": "Explanation ......", "accountInfoTitle": "Your account information", - "adminInfoTitle": "Primary administrator's information", - "createdInfo": "Your account has been created and a verification email has been set to your registered email address. Please click on the verification link included in the email to activate your account." + "adminInfoTitle": "Primary administrator's information" }, "label": { "company": "Company Name", @@ -62,12 +70,15 @@ "adminName": "Admin Name", "email": "Email", "password": "Password", - "title": "Account created" + "signupButton": "Sign up" } }, "signupCompletePage": { + "text": { + "createdInfo": "Your account has been created and a verification email has been set to your registered email address. Please click on the verification link included in the email to activate your account." + }, "label": { - "signupButton": "Sign up" + "title": "Account created" } }, "signupVerifyPage": { @@ -83,4 +94,4 @@ "returnToSignIn": "Return to Sign in" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 692de3e..7f62fba 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -3,7 +3,8 @@ "message": { "inputEmptyError": "(es)Error Message", "passwordIncorrectError": "(es)Error Message", - "emailIncorrectError": "(es)Error Message" + "emailIncorrectError": "(es)Error Message", + "internalServerError": "(es)処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。" }, "label": { "cancel": "(es)Cancel", @@ -25,6 +26,11 @@ } }, "signupPage": { + "message": { + "inputEmptyError": "(es)この項目の入力は必須です。入力してください。", + "passwordIncorrectError": "(es)入力されたパスワードがルールを満たしていません。下記のルールを満たすパスワードを入力してください。", + "emailIncorrectError": "(es)メールアドレスの形式が不正です。正しいメールアドレスの形式で入力してください。" + }, "text": { "title": "(es)Create your account", "pageExplanation": "(es)Explanation...", @@ -48,12 +54,14 @@ } }, "signupConfirmPage": { + "message": { + "emailConflictError": "(es)このメールアドレスは既に登録されています。他のメールアドレスで登録してください。" + }, "text": { "title": "(es)Confirmation", "pageExplanation": "(es)Explanation ......", "accountInfoTitle": "(es)Your account information", - "adminInfoTitle": "(es)Primary administrator's information", - "createdInfo": "(es)Your account has been created and a verification email has been set to your registered email address. Please click on the verification link included in the email to activate your account." + "adminInfoTitle": "(es)Primary administrator's information" }, "label": { "company": "(es)Company Name", @@ -62,12 +70,15 @@ "adminName": "(es)Admin Name", "email": "(es)Email", "password": "(es)Password", - "title": "(es)Account created" + "signupButton": "(es)Sign up" } }, "signupCompletePage": { + "text": { + "createdInfo": "(es)Your account has been created and a verification email has been set to your registered email address. Please click on the verification link included in the email to activate your account." + }, "label": { - "signupButton": "(es)Sign up" + "title": "(es)Account created" } }, "signupVerifyPage": { @@ -83,4 +94,4 @@ "returnToSignIn": "(es)Return to Sign in" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 15c6324..862cf50 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -3,7 +3,8 @@ "message": { "inputEmptyError": "(fr)Error Message", "passwordIncorrectError": "(fr)Error Message", - "emailIncorrectError": "(fr)Error Message" + "emailIncorrectError": "(fr)Error Message", + "internalServerError": "(fr)処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。" }, "label": { "cancel": "(fr)Cancel", @@ -25,6 +26,11 @@ } }, "signupPage": { + "message": { + "inputEmptyError": "(fr)この項目の入力は必須です。入力してください。", + "passwordIncorrectError": "(fr)入力されたパスワードがルールを満たしていません。下記のルールを満たすパスワードを入力してください。", + "emailIncorrectError": "(fr)メールアドレスの形式が不正です。正しいメールアドレスの形式で入力してください。" + }, "text": { "title": "(fr)Create your account", "pageExplanation": "(fr)Explanation...", @@ -48,12 +54,14 @@ } }, "signupConfirmPage": { + "message": { + "emailConflictError": "(fr)このメールアドレスは既に登録されています。他のメールアドレスで登録してください。" + }, "text": { "title": "(fr)Confirmation", "pageExplanation": "(fr)Explanation ......", "accountInfoTitle": "(fr)Your account information", - "adminInfoTitle": "(fr)Primary administrator's information", - "createdInfo": "(fr)Your account has been created and a verification email has been set to your registered email address. Please click on the verification link included in the email to activate your account." + "adminInfoTitle": "(fr)Primary administrator's information" }, "label": { "company": "(fr)Company Name", @@ -62,12 +70,15 @@ "adminName": "(fr)Admin Name", "email": "(fr)Email", "password": "(fr)Password", - "title": "(fr)Account created" + "signupButton": "(fr)Sign up" } }, "signupCompletePage": { + "text": { + "createdInfo": "(fr)Your account has been created and a verification email has been set to your registered email address. Please click on the verification link included in the email to activate your account." + }, "label": { - "signupButton": "(fr)Sign up" + "title": "(fr)Account created" } }, "signupVerifyPage": { @@ -83,4 +94,4 @@ "returnToSignIn": "(fr)Return to Sign in" } } -} +} \ No newline at end of file