From cfaad1641f437133f70b2fc34891f572cdf7e188 Mon Sep 17 00:00:00 2001 From: "sosuke.iwabuchi" Date: Fri, 12 May 2023 10:55:11 +0900 Subject: [PATCH] =?UTF-8?q?=E8=AA=8D=E8=A8=BC=E6=A9=9F=E8=83=BD=E6=8B=A1?= =?UTF-8?q?=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 7 + package.json | 11 +- src/@types/index.ts | 2 + src/App.tsx | 36 +-- src/api/auth.ts | 200 ++++++++++++++++ src/api/index.ts | 234 +++++++++++++++++++ src/api/url.ts | 25 ++ src/codes/page.ts | 18 +- src/components/form/CheckBoxCustom.tsx | 83 +++++++ src/components/form/DatePickerCustom.tsx | 45 ++++ src/components/form/InputAlert.tsx | 56 +++++ src/components/form/TextFieldCustom.tsx | 95 ++++++++ src/components/form/TextFieldEx.tsx | 36 +++ src/components/hook-form/FormProvider.tsx | 24 ++ src/components/hook-form/RHFAutoComplete.tsx | 121 ++++++++++ src/components/hook-form/RHFCheckbox.tsx | 96 ++++++++ src/components/hook-form/RHFDatePicker.tsx | 106 +++++++++ src/components/hook-form/RHFRadioGroup.tsx | 53 +++++ src/components/hook-form/RHFSelect.tsx | 119 ++++++++++ src/components/hook-form/RHFSwitch.tsx | 29 +++ src/components/hook-form/RHFTextField.tsx | 66 ++++++ src/components/hook-form/index.ts | 33 +++ src/components/table/TableHeadCustom.tsx | 96 ++++++++ src/components/table/index.ts | 1 + src/config.ts | 3 + src/contexts/AuthContext.tsx | 126 ++++++++++ src/contexts/DashboardLayoutContext.tsx | 32 ++- src/contexts/PageContext.tsx | 12 +- src/contexts/SearchConditionContext.tsx | 131 +++++++++++ src/hooks/useAPICall.ts | 168 +++++++++++++ src/hooks/useAuth.ts | 7 + src/hooks/useDashBoard.ts | 10 +- src/hooks/useNavigateCustom.ts | 1 + src/hooks/useSearchConditionContext.ts | 6 + src/hooks/useSnackbarCustom.ts | 26 +++ src/hooks/useTable.ts | 177 ++++++++++++++ src/hooks/useURLSearchParams.ts | 48 ++++ src/index.tsx | 17 +- src/layouts/dashbord/index.tsx | 17 +- src/layouts/dashbord/navigator.tsx | 70 +++--- src/layouts/dashbord/tab/ContractTabs.tsx | 6 +- src/layouts/dashbord/tab/tabutil.tsx | 14 +- src/layouts/simple/index.tsx | 26 +++ src/pages/auth/login.tsx | 80 +++++++ src/pages/auth/logout.tsx | 21 ++ src/pages/dashboard/contract/detail.tsx | 5 +- src/pages/dashboard/contract/list.tsx | 179 +++++++++++++- src/providers/CsrfTokenProvider.tsx | 17 ++ src/providers/SnackbarProvider.tsx | 9 + src/routes/index.tsx | 53 +++-- src/routes/path.ts | 89 ++++--- src/theme/index.tsx | 15 +- src/types/common.ts | 11 + src/types/contract.ts | 5 + src/utils/axios.ts | 20 ++ src/utils/datetime.ts | 24 ++ tsconfig.json | 2 +- yarn.lock | 173 +++++++++++++- 58 files changed, 3033 insertions(+), 159 deletions(-) create mode 100644 .env create mode 100644 src/api/auth.ts create mode 100644 src/api/index.ts create mode 100644 src/api/url.ts create mode 100644 src/components/form/CheckBoxCustom.tsx create mode 100644 src/components/form/DatePickerCustom.tsx create mode 100644 src/components/form/InputAlert.tsx create mode 100644 src/components/form/TextFieldCustom.tsx create mode 100644 src/components/form/TextFieldEx.tsx create mode 100644 src/components/hook-form/FormProvider.tsx create mode 100644 src/components/hook-form/RHFAutoComplete.tsx create mode 100644 src/components/hook-form/RHFCheckbox.tsx create mode 100644 src/components/hook-form/RHFDatePicker.tsx create mode 100644 src/components/hook-form/RHFRadioGroup.tsx create mode 100644 src/components/hook-form/RHFSelect.tsx create mode 100644 src/components/hook-form/RHFSwitch.tsx create mode 100644 src/components/hook-form/RHFTextField.tsx create mode 100644 src/components/hook-form/index.ts create mode 100644 src/components/table/TableHeadCustom.tsx create mode 100644 src/components/table/index.ts create mode 100644 src/config.ts create mode 100644 src/contexts/AuthContext.tsx create mode 100644 src/contexts/SearchConditionContext.tsx create mode 100644 src/hooks/useAPICall.ts create mode 100644 src/hooks/useAuth.ts create mode 100644 src/hooks/useSearchConditionContext.ts create mode 100644 src/hooks/useSnackbarCustom.ts create mode 100644 src/hooks/useTable.ts create mode 100644 src/hooks/useURLSearchParams.ts create mode 100644 src/layouts/simple/index.tsx create mode 100644 src/pages/auth/login.tsx create mode 100644 src/pages/auth/logout.tsx create mode 100644 src/providers/CsrfTokenProvider.tsx create mode 100644 src/providers/SnackbarProvider.tsx create mode 100644 src/types/common.ts create mode 100644 src/types/contract.ts create mode 100644 src/utils/axios.ts create mode 100644 src/utils/datetime.ts diff --git a/.env b/.env new file mode 100644 index 0000000..dfec92f --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +GENERATE_SOURCEMAP=true + +PORT=8080 + +# HOST +# ローカル +REACT_APP_HOST_API_KEY=http://localhost diff --git a/package.json b/package.json index 2ecdbf3..52da025 100644 --- a/package.json +++ b/package.json @@ -5,24 +5,33 @@ "dependencies": { "@emotion/react": "^11.10.8", "@emotion/styled": "^11.10.8", + "@hookform/resolvers": "^3.1.0", "@mui/icons-material": "^5.11.16", + "@mui/lab": "^5.0.0-alpha.129", "@mui/material": "^5.12.2", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^13.0.0", "@testing-library/user-event": "^13.2.1", + "@types/axios": "^0.14.0", + "@types/date-fns": "^2.6.0", "@types/jest": "^27.0.1", "@types/lodash": "^4.14.194", "@types/node": "^16.7.13", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/react-router-dom": "^5.3.3", + "axios": "^1.4.0", + "date-fns": "^2.30.0", "lodash": "^4.17.21", + "notistack": "^3.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.43.9", "react-router-dom": "^6.11.0", "react-scripts": "5.0.1", "typescript": "^4.4.2", - "web-vitals": "^2.1.0" + "web-vitals": "^2.1.0", + "yup": "^1.1.1" }, "scripts": { "start": "react-scripts start", diff --git a/src/@types/index.ts b/src/@types/index.ts index e92f852..86d528a 100644 --- a/src/@types/index.ts +++ b/src/@types/index.ts @@ -1,5 +1,7 @@ import { ReactNode } from "react"; +export type DataUrl = string; + export type HasChildren = { children: ReactNode; }; diff --git a/src/App.tsx b/src/App.tsx index 8d7457a..eae4fa7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,25 +1,29 @@ import { CssBaseline } from "@mui/material"; +import AuthContextProvider from "contexts/AuthContext"; import { PageContextProvider } from "contexts/PageContext"; import { WindowSizeContextProvider } from "contexts/WindowSizeContext"; +import CsrfTokenProvider from "providers/CsrfTokenProvider"; +import SnackbarProvider from "providers/SnackbarProvider"; import { BrowserRouter } from "react-router-dom"; import { Routes } from "routes"; -import { AppThemeProvider } from "theme"; +import AppThemeProvider from "theme"; -function App() { +export default function App() { return ( - <> - - - - - - - - - - - + + + + + + + + + + + + + + + ); } - -export default App; diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 0000000..f94b7d7 --- /dev/null +++ b/src/api/auth.ts @@ -0,0 +1,200 @@ +import { UserRole } from "codes/user"; +import { APICommonResponse, ApiId, HttpMethod, request } from "."; +import { getUrl } from "./url"; + +type MeResponse = { + data: { + id: string; + contract_id: string; + role: UserRole; + }; +} & APICommonResponse; + +export const csrfToken = async () => { + await request({ + url: getUrl(ApiId.CSRF_TOKEN), + method: HttpMethod.GET, + }); +}; + +export const me = async () => { + const res = await request({ + url: getUrl(ApiId.ME), + method: HttpMethod.GET, + }); + return res; +}; + +export const login = async (param: { email: string; password: string }) => { + const res = await request({ + url: getUrl(ApiId.LOGIN), + method: HttpMethod.POST, + data: param, + }); + return res; +}; + +export const logout = async () => { + const res = await request({ + url: getUrl(ApiId.LOGOUT), + method: HttpMethod.GET, + }); + return res; +}; +// export const getMe = async () => { +// const res = await request({ +// url: getUrl(ApiId.ME), +// method: HttpMethod.GET, +// }); +// return res; +// }; + +// export const login = async (email: string, password: string) => { +// const data = new URLSearchParams({ +// email, +// password, +// }); +// const res = await request({ +// url: getUrl(ApiId.NORMAL_LOGIN), +// method: HttpMethod.POST, +// data, +// }); +// return res; +// }; + +// export const loginAdmin = async (email: string, password: string) => { +// const data = new URLSearchParams({ +// email, +// password, +// }); +// const res = await request({ +// url: getUrl(ApiId.ADMIN_LOGIN), +// method: HttpMethod.POST, +// data, +// }); +// return res; +// }; + +// export const logout = async () => { +// const res = await request({ +// url: getUrl(ApiId.LOGOUT), +// method: HttpMethod.POST, +// }); +// return res; +// }; + +// export type StartEmailVerifyParams = { +// email: string; +// for_entry?: boolean; +// customer_code?: string; +// parking_management_code?: string; +// }; + +// export const startEmailVerify = async (data: StartEmailVerifyParams) => { +// const sendData = new URLSearchParams(makeParam(data)); +// const res = await request({ +// url: getUrl(ApiId.EMAIL_VERIFY_START), +// method: HttpMethod.POST, +// data: sendData, +// }); +// return res; +// }; + +// export const verifyEmail = async ({ token }: { token: string }) => { +// const sendData = new URLSearchParams(makeParam({ token })); +// const res = await request({ +// url: getUrl(ApiId.EMAIL_VERIFY), +// method: HttpMethod.POST, +// data: sendData, +// }); +// return res; +// }; + +// // パスワードリセット開始 +// export type ResetPasswordStartParams = { +// email: string; +// customer_code?: string; +// parking_management_code?: string; +// }; +// export const resetPasswordStart = async (data: ResetPasswordStartParams) => { +// const sendData = new URLSearchParams(makeParam(data)); +// const res = await request({ +// url: getUrl(ApiId.RESET_PASSWORD_START), +// method: HttpMethod.POST, +// data: sendData, +// }); +// return res; +// }; + +// // パスワードリセットトークンチェック +// export type ResetPasswordVerifyParams = { +// token: string; +// }; +// export const resetPasswordVerify = async (data: ResetPasswordVerifyParams) => { +// const sendData = new URLSearchParams(makeParam(data)); +// const res = await request({ +// url: getUrl(ApiId.RESET_PASSWORD_VERIFY), +// method: HttpMethod.POST, +// data: sendData, +// }); +// return res; +// }; + +// // パスワードリセット +// export type ResetPasswordParams = { +// password: string; +// token: string; +// }; +// export const resetPassword = async (data: ResetPasswordParams) => { +// const sendData = new URLSearchParams(makeParam(data)); +// const res = await request({ +// url: getUrl(ApiId.RESET_PASSWORD), +// method: HttpMethod.POST, +// data: sendData, +// }); +// return res; +// }; + +// // 利用者登録 +// export const RegisterUserParamKeyName = { +// TOKEN: 'token', +// PASSWORD: 'password', +// FIRST_NAME: 'first_name', +// LAST_NAME: 'last_name', +// FIRST_NAME_KANA: 'first_name_kana', +// LAST_NAME_KANA: 'last_name_kana', +// ZIP_CODE: 'zip_code', +// PREF_CODE: 'pref_code', +// ADDRESS1: 'address1', +// ADDRESS2: 'address2', +// ADDRESS3: 'address3', +// PHONE_NUMBER: 'phone_number', +// CONFIRM_PRIVACY_POLICY: 'confirm_privacy_policy', +// } as const; +// export type RegisterUserParamKeyName = +// typeof RegisterUserParamKeyName[keyof typeof RegisterUserParamKeyName]; + +// export type RegisterUserParam = { +// [RegisterUserParamKeyName.TOKEN]: string; +// [RegisterUserParamKeyName.PASSWORD]: string; +// [RegisterUserParamKeyName.FIRST_NAME]: string; +// [RegisterUserParamKeyName.LAST_NAME]: string; +// [RegisterUserParamKeyName.FIRST_NAME_KANA]: string; +// [RegisterUserParamKeyName.LAST_NAME_KANA]: string; +// [RegisterUserParamKeyName.ZIP_CODE]: string; +// [RegisterUserParamKeyName.PREF_CODE]: string; +// [RegisterUserParamKeyName.ADDRESS1]: string; +// [RegisterUserParamKeyName.ADDRESS2]: string; +// [RegisterUserParamKeyName.ADDRESS3]: string; +// [RegisterUserParamKeyName.PHONE_NUMBER]: string; +// [RegisterUserParamKeyName.CONFIRM_PRIVACY_POLICY]: boolean; +// }; +// export const registerUser = async (data: RegisterUserParam) => { +// const sendData = new URLSearchParams(makeParam(data)); +// const res = await request({ +// url: getUrl(ApiId.REGISTER_USER), +// method: HttpMethod.POST, +// data: sendData, +// }); +// return res; +// }; diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..e60980d --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,234 @@ +import { AxiosError, AxiosResponse } from "axios"; +import { format } from "date-fns"; +import { Dictionary, get } from "lodash"; +import { DataUrl } from "@types"; +import { setFormErrorMessages } from "components/hook-form"; +import axios from "utils/axios"; + +let id = 0; +export const ApiId = { + CSRF_TOKEN: id++, + ME: id++, + + LOGIN: id++, + LOGOUT: id++, +} as const; +export type ApiId = (typeof ApiId)[keyof typeof ApiId]; + +export const HttpMethod = { + GET: "get", + POST: "post", +} as const; +export type HttpMethod = (typeof HttpMethod)[keyof typeof HttpMethod]; + +export const ResultCode = { + SUCCESS: 0, + FAILED: 1, + UNAUTHORIZED: 2, + EXCLUSIVE_ERROR: 3, +}; +export type ResultCode = (typeof ResultCode)[keyof typeof ResultCode]; + +export interface APICommonResponse { + result: ResultCode; + messages: { + errors?: Dictionary; + general?: string; + email_id?: number; + }; +} +export type ImagesResponse = { + data: { + images: DataUrl[]; + }; +} & APICommonResponse; + +export const makeParam = (data: T): Dictionary => { + const res: Dictionary = {}; + + Object.keys(data).map((key) => { + const val = get(data, key); + if (typeof val === "string" && val.length !== 0) { + res[key] = val; + } else if (typeof val === "number") { + res[key] = String(val); + } else if (typeof val === "boolean") { + res[key] = val ? "1" : "0"; + } else if (val instanceof Date) { + res[key] = format(val, "yyyy-MM-dd"); + } + }); + + return res; +}; + +export const makeFormData = (data: T): FormData => { + const res = new FormData(); + + Object.keys(data).map((key) => { + const val = get(data, key); + if (typeof val === "string" && val.length !== 0) { + res.append(key, val); + } else if (typeof val === "number") { + res.append(key, String(val)); + } else if (typeof val === "boolean") { + res.append(key, val ? "1" : "0"); + } else if (val instanceof Date) { + res.append(key, format(val, "yyyy-MM-dd")); + } else if (val instanceof File) { + res.append(key, val); + } else if (Array.isArray(val)) { + val.forEach((v) => { + res.append(key + "[]", v); + }); + } else { + console.log("undefined data", key, val); + } + }); + + return res; +}; + +const isAxiosError = (error: any): error is AxiosError => { + return !!error.isAxiosError; +}; + +type RequestArgument = { + url: string; + method: HttpMethod; + data?: URLSearchParams | FormData | object; + multipart?: boolean; +}; +export const request = async ({ + url, + method, + data, + multipart, +}: RequestArgument): Promise => { + let response: AxiosResponse | null = null; + if (data instanceof URLSearchParams) { + data.sort(); + } + try { + if (multipart && data instanceof FormData) { + response = await axios({ + url, + method: "post", + data, + headers: { + "content-type": "multipart/form-data", + }, + }); + console.log("RESPONSE", url, "multipart", data, response?.data); + } else if (method === HttpMethod.GET) { + let searchUrl = url; + + if (data instanceof URLSearchParams) { + searchUrl += "?" + data.toString(); + } + response = await axios.get(searchUrl); + console.log("RESPONSE", searchUrl, method, response?.data); + } else if (method === HttpMethod.POST) { + response = await axios.post(url, data); + + let sendData: Dictionary = {}; + if (data instanceof URLSearchParams) { + data.forEach((val, key) => { + sendData[key] = val; + }); + } else if (typeof data === "object" && !(data instanceof FormData)) { + Object.keys(data).forEach((key) => { + sendData[key] = get(data, key); + }); + } + console.log("RESPONSE", url, method, sendData, response?.data); + } else { + return null; + } + + if (response && response.data.result === ResultCode.SUCCESS) { + return response.data; + } + return response?.data; + } catch (e) { + if (isAxiosError(e)) { + if (e.response?.status === 401 || e.response?.status === 419) { + window.location.reload(); + } + } + if (typeof e === "object") { + const message = get(e, "message"); + if (message === "Unauthenticated.") { + console.log("401!!!"); + window.location.reload(); + return null; + } + if (message === "CSRF token mismatch.") { + console.log("419!!!"); + window.location.reload(); + return null; + } + if (message === "Service Unavailable") { + console.log("503!!!"); + window.location.reload(); + return null; + } + } + throw e; + } +}; + +export async function apiRequest< + T extends APICommonResponse, + U extends object +>({ + apiMethod, + sendData, + onSuccess, + onFailed, + onFinaly, + errorSetter, + setSending, +}: { + apiMethod: (sendData: U) => Promise; + sendData: U; + onSuccess?: (res: T, sendData: U) => void; + onFailed?: (res: APICommonResponse | null) => void; + onFinaly?: (res: APICommonResponse | null) => void; + errorSetter?: any; + setSending?: (sending: boolean) => void; +}) { + if (setSending) { + setSending(true); + } + const res = await apiMethod(sendData); + if (setSending) { + setSending(false); + } + + if (res?.result === ResultCode.SUCCESS) { + if (onSuccess) { + onSuccess(res, sendData); + } + } else { + if (res?.messages.errors) { + if (errorSetter) { + const errorCount = setFormErrorMessages( + sendData, + errorSetter, + res.messages.errors + ); + console.log("FormErrorCount", errorCount); + } + } + if (onFailed) { + onFailed(res); + } + } + + if (onFinaly) { + onFinaly(res); + } + + return res; +} diff --git a/src/api/url.ts b/src/api/url.ts new file mode 100644 index 0000000..fc3b347 --- /dev/null +++ b/src/api/url.ts @@ -0,0 +1,25 @@ +import { ApiId } from "."; + +const urls = { + [ApiId.CSRF_TOKEN]: "sanctum/csrf-cookie", + [ApiId.ME]: "me", + [ApiId.LOGIN]: "login", + [ApiId.LOGOUT]: "logout", +}; + +const prefixs = { + [ApiId.CSRF_TOKEN]: "", +}; +const DEFAULT_API_URL_PREFIX = "api"; + +const getPrefix = (apiId: ApiId) => { + return prefixs[apiId] ?? DEFAULT_API_URL_PREFIX; +}; + +export const getUrl = (apiId: ApiId) => { + let url = getPrefix(apiId); + if (url.length !== 0) { + url += "/"; + } + return url + (urls[apiId] ?? ""); +}; diff --git a/src/codes/page.ts b/src/codes/page.ts index 6567a51..dbf9e73 100644 --- a/src/codes/page.ts +++ b/src/codes/page.ts @@ -1,8 +1,20 @@ +let id = 0; export const PageID = { - NONE: "NONE", + NONE: id++, - DASHBOARD_CONTRACT_LIST: "DASHBOARD_CONTRACT_LIST", - DASHBOARD_CONTRACT_DETAIL: "DASHBOARD_CONTRACT_DETAIL", + LOGIN: id++, + LOGOUT: id++, + + DASHBOARD_CONTRACT_LIST: id++, + DASHBOARD_CONTRACT_DETAIL: id++, } as const; export type PageID = (typeof PageID)[keyof typeof PageID]; + +id = 0; +export const TabID = { + NONE: id++, + A: id++, +} as const; + +export type TabID = (typeof TabID)[keyof typeof TabID]; diff --git a/src/components/form/CheckBoxCustom.tsx b/src/components/form/CheckBoxCustom.tsx new file mode 100644 index 0000000..4511b30 --- /dev/null +++ b/src/components/form/CheckBoxCustom.tsx @@ -0,0 +1,83 @@ +import { Checkbox, CheckboxProps, FormControlLabel } from "@mui/material"; +import { Dictionary } from "@types"; +import { useMemo, useState } from "react"; + +export type CheckBoxCustomProps = { + label: string; + value: boolean; + onFix?: () => void; + onChangeValue?: (val: boolean) => void; + messages?: Dictionary; + readonly?: boolean; +} & CheckboxProps; + +export default function CheckBoxCustom({ + label, + value, + onFix, + onChangeValue, + messages, + readonly, + ...others +}: CheckBoxCustomProps) { + const [oldValue, setOldValue] = useState(null); + + const inputProps = useMemo(() => { + if (readonly) { + return { + style: { color: "rgb(50, 50, 50)" }, + disabled: true, + }; + } else { + return undefined; + } + }, [readonly]); + + const fix = (newValue: string) => { + if (oldValue !== newValue) { + setOldValue(newValue); + if (onFix) { + onFix(); + } + } + }; + + const handleChange = (e: any, val: boolean) => { + if (onChangeValue) { + onChangeValue(val); + } + }; + + const message = useMemo(() => { + if (messages && others.name) { + return messages[others.name] ?? ""; + } else { + return ""; + } + }, [messages]); + + const error = useMemo(() => { + if (messages && others.name) { + return ( + messages[others.name] !== undefined && + messages[others.name].length !== 0 + ); + } else { + return false; + } + }, [messages]); + + return ( + + } + label={label} + /> + ); +} diff --git a/src/components/form/DatePickerCustom.tsx b/src/components/form/DatePickerCustom.tsx new file mode 100644 index 0000000..7542ea5 --- /dev/null +++ b/src/components/form/DatePickerCustom.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { TextField, TextFieldProps } from '@mui/material'; +import { DatePicker } from '@mui/lab'; +import { TextFieldCustomProps } from './TextFieldCustom'; +import { isValid } from 'date-fns'; + +type DatePickerCustomProps = TextFieldCustomProps & { + value: Date | null; + onChangeDate: (val: Date | null) => void; +}; + +const DatePickerCustom = ({ label, value, onChangeDate, ...others }: DatePickerCustomProps) => { + const handleChange = (val: string | null) => { + console.log({ handleChange: val }); + if (onChangeDate) { + if (val !== null) { + const date = new Date(val); + if (isValid(date)) { + onChangeDate(date); + } else { + onChangeDate(null); + } + } else { + onChangeDate(null); + } + } + }; + + const handleRender = (params: TextFieldProps) => { + return ; + }; + + return ( + + ); +}; + +export default React.memo(DatePickerCustom); diff --git a/src/components/form/InputAlert.tsx b/src/components/form/InputAlert.tsx new file mode 100644 index 0000000..144af10 --- /dev/null +++ b/src/components/form/InputAlert.tsx @@ -0,0 +1,56 @@ +import { Alert, SxProps } from "@mui/material"; +import { APIErrorType } from "hooks/useAPICall"; +import React, { useEffect, useMemo, useRef } from "react"; + +type Props = { + error: APIErrorType; + sx?: SxProps; + getMessage?: (errpr: APIErrorType) => string | null; + message?: string; + errorScroll?: boolean; +}; +const InputAlert = ({ + error, + sx, + getMessage, + message: errorMessage, + errorScroll, +}: Props) => { + const ref = useRef(null); + + const message = useMemo(() => { + if (errorMessage) { + return errorMessage; + } + if (getMessage) { + const m = getMessage(error); + if (m !== null) { + return m; + } + } + if (error === APIErrorType.INPUT) return "入力項目を確認してください。"; + if (error === APIErrorType.SERVER) + return "エラーが発生しております。しばらくお待ちください。"; + if (error === APIErrorType.EXCLUSIVE) + return "ページの期限が切れています。再度読込を行ってください"; + return ""; + }, [error, errorMessage, getMessage]); + + // エラー時に自動的にスクロール制御 + useEffect(() => { + if (error !== APIErrorType.NONE && errorScroll) { + if (ref.current) { + ref.current.scrollIntoView({ block: "center", behavior: "smooth" }); + } + } + }, [error, ref]); + + if (message === "" && error === APIErrorType.NONE) return null; + return ( + + {message} + + ); +}; + +export default React.memo(InputAlert); diff --git a/src/components/form/TextFieldCustom.tsx b/src/components/form/TextFieldCustom.tsx new file mode 100644 index 0000000..20a8e53 --- /dev/null +++ b/src/components/form/TextFieldCustom.tsx @@ -0,0 +1,95 @@ +import React, { useMemo, useState } from "react"; +import { TextField, TextFieldProps } from "@mui/material"; +import { Dictionary } from "@types"; + +export type TextFieldCustomProps = { + onFix?: () => void; + onChangeValue?: (val: string) => void; + messages?: Dictionary; + readonly?: boolean; +} & TextFieldProps; + +export default function TextFieldCustom({ + onFix, + onChangeValue, + messages, + readonly, + ...others +}: TextFieldCustomProps) { + const [oldValue, setOldValue] = useState(null); + + const inputProps = useMemo(() => { + if (readonly) { + return { + style: { color: "rgb(50, 50, 50)" }, + disabled: true, + }; + } else { + return undefined; + } + }, [readonly]); + + const fix = (newValue: string) => { + if (oldValue !== newValue) { + setOldValue(newValue); + if (onFix) { + onFix(); + } + } + }; + + const handleEnter = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + if (e.target instanceof HTMLInputElement) { + fix(e.target.value); + } + } + }; + + const handleBlur = (e: React.FocusEvent) => { + if (!others.select) { + fix(e.target.value); + } + }; + + const handleChange = (e: React.ChangeEvent) => { + if (onChangeValue) { + onChangeValue(e.target.value); + } + if (others.select) { + fix(e.target.value); + } + }; + + const message = useMemo(() => { + if (messages && others.name) { + return messages[others.name] ?? ""; + } else { + return ""; + } + }, [messages]); + + const error = useMemo(() => { + if (messages && others.name) { + return ( + messages[others.name] !== undefined && + messages[others.name].length !== 0 + ); + } else { + return false; + } + }, [messages]); + + return ( + + ); +} diff --git a/src/components/form/TextFieldEx.tsx b/src/components/form/TextFieldEx.tsx new file mode 100644 index 0000000..398d73b --- /dev/null +++ b/src/components/form/TextFieldEx.tsx @@ -0,0 +1,36 @@ +import React, { useMemo, useState } from 'react'; +import { TextField, TextFieldProps } from '@mui/material'; + +export type TextFieldExProps = { + readOnly?: boolean; +} & TextFieldProps; + +const TextFieldEx = ({ readOnly, ...others }: TextFieldExProps) => { + if (readOnly) { + const props: any = {}; + if (typeof others.value === 'string' && others.value.length === 0) { + props.value = ' '; + } + + return ( + + ); + } + + return ; +}; + +export default React.memo(TextFieldEx); diff --git a/src/components/hook-form/FormProvider.tsx b/src/components/hook-form/FormProvider.tsx new file mode 100644 index 0000000..5434e3b --- /dev/null +++ b/src/components/hook-form/FormProvider.tsx @@ -0,0 +1,24 @@ +import { Button } from '@mui/material'; +import { ReactNode } from 'react'; +// form +import { FormProvider as Form, UseFormReturn } from 'react-hook-form'; + +// ---------------------------------------------------------------------- + +type Props = { + children: ReactNode; + methods: UseFormReturn; + onSubmit?: VoidFunction; +}; + +export default function FormProvider({ children, onSubmit, methods }: Props) { + return ( +
+ + {children} + {/* エンターでsubmitできるようにする */} +