(
+
+
+ {options.map((option) => (
+ }
+ label={option.label}
+ />
+ ))}
+
+
+ {!!error && (
+
+ {error.message}
+
+ )}
+
+ )}
+ />
+ );
+}
diff --git a/src/components/hook-form/RHFSelect.tsx b/src/components/hook-form/RHFSelect.tsx
new file mode 100644
index 0000000..4dfa0ab
--- /dev/null
+++ b/src/components/hook-form/RHFSelect.tsx
@@ -0,0 +1,119 @@
+import { useFormContext, Controller } from "react-hook-form";
+import { MenuItem, TextField, TextFieldProps } from "@mui/material";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import TextFieldEx from "../form/TextFieldEx";
+import { Dictionary } from "@types";
+
+// ----------------------------------------------------------------------
+
+export type SelectOptionProps = {
+ value: string;
+ label: string;
+};
+
+export const makeOptions = (list: Dictionary[]): SelectOptionProps[] => {
+ const ret: SelectOptionProps[] = [];
+ list.forEach((dic) => {
+ const value = Object.keys(dic)[0];
+ const label = dic[value];
+ ret.push({
+ value,
+ label,
+ });
+ });
+ return ret;
+};
+
+type IProps = {
+ name: string;
+ children?: any;
+ readOnly?: boolean;
+ options?: SelectOptionProps[];
+ onFix?: VoidFunction;
+};
+
+type Props = IProps & TextFieldProps;
+export type RHFSelectProps = Props;
+
+export default function RHFSelect({
+ name,
+ children,
+ readOnly,
+ options,
+ onFix,
+ ...other
+}: Props) {
+ const { control, watch } = useFormContext();
+
+ const formValue: string = watch(name);
+ const [isMounted, setIsMounted] = useState(false);
+
+ const getOptionElements = useCallback(() => {
+ if (options) {
+ if (options.length === 0) {
+ return [
+ ,
+ ];
+ } else {
+ return options.map((option, index) => {
+ return (
+
+ );
+ });
+ }
+ }
+ return [];
+ }, [options]);
+
+ const getLabel = useMemo(() => {
+ if (!options) return "";
+ return (
+ options.find((option) => {
+ return option.value === formValue;
+ })?.label ?? " "
+ );
+ }, [formValue, options]);
+
+ useEffect(() => {
+ if (isMounted && onFix) {
+ onFix();
+ }
+ setIsMounted(true);
+ }, [formValue]);
+
+ if (readOnly) {
+ return (
+
+ );
+ }
+
+ if (!options) {
+ return null;
+ }
+
+ return (
+ (
+
+ {children}
+ {getOptionElements()}
+
+ )}
+ />
+ );
+}
diff --git a/src/components/hook-form/RHFSwitch.tsx b/src/components/hook-form/RHFSwitch.tsx
new file mode 100644
index 0000000..a56c548
--- /dev/null
+++ b/src/components/hook-form/RHFSwitch.tsx
@@ -0,0 +1,29 @@
+// form
+import { useFormContext, Controller } from 'react-hook-form';
+// @mui
+import { Switch, FormControlLabel, FormControlLabelProps } from '@mui/material';
+
+// ----------------------------------------------------------------------
+
+type IProps = Omit;
+
+interface Props extends IProps {
+ name: string;
+}
+
+export default function RHFSwitch({ name, ...other }: Props) {
+ const { control } = useFormContext();
+
+ return (
+ }
+ />
+ }
+ {...other}
+ />
+ );
+}
diff --git a/src/components/hook-form/RHFTextField.tsx b/src/components/hook-form/RHFTextField.tsx
new file mode 100644
index 0000000..e00ec30
--- /dev/null
+++ b/src/components/hook-form/RHFTextField.tsx
@@ -0,0 +1,66 @@
+import { useFormContext, Controller } from "react-hook-form";
+import { TextField, TextFieldProps } from "@mui/material";
+import { useMemo } from "react";
+import { formatDateStr } from "utils/datetime";
+import TextFieldEx from "../form/TextFieldEx";
+
+// ----------------------------------------------------------------------
+
+type IProps = {
+ name: string;
+ readOnly?: boolean;
+};
+
+type Props = IProps & TextFieldProps;
+export type RHFTextFieldProps = Props;
+
+export default function RHFTextField({
+ name,
+ readOnly,
+ size: fieldSize = "small",
+ ...other
+}: Props) {
+ const { control, watch } = useFormContext();
+
+ const value = watch(name);
+
+ const valueStr = useMemo(() => {
+ if (typeof value === "string") {
+ if (value === "") {
+ return " ";
+ } else {
+ return value;
+ }
+ }
+ if (value instanceof Date) {
+ return formatDateStr(value);
+ }
+ if (readOnly) {
+ return " ";
+ }
+ return "";
+ }, [value]);
+
+ if (readOnly) {
+ return (
+
+ );
+ }
+
+ return (
+ (
+
+ )}
+ />
+ );
+}
diff --git a/src/components/hook-form/index.ts b/src/components/hook-form/index.ts
new file mode 100644
index 0000000..de2f8e7
--- /dev/null
+++ b/src/components/hook-form/index.ts
@@ -0,0 +1,33 @@
+import { Dictionary } from "@types";
+
+export * from "./RHFCheckbox";
+
+export { default as FormProvider } from "./FormProvider";
+
+export { default as RHFSwitch } from "./RHFSwitch";
+export { default as RHFSelect } from "./RHFSelect";
+export { default as RHFTextField } from "./RHFTextField";
+export { default as RHFRadioGroup } from "./RHFRadioGroup";
+export { default as RHFAutoComplete } from "./RHFAutoComplete";
+
+/**
+ *
+ * @param formData object
+ * @param setter RHFの関数setError
+ * @param messages Dictionary
+ */
+export function setFormErrorMessages(
+ formData: object,
+ setter: any,
+ messages: Dictionary
+) {
+ let count = 0;
+ const keys = Object.keys(formData);
+ Object.keys(messages).forEach((name) => {
+ if (keys.includes(name)) {
+ setter(name, { message: messages[name] });
+ count++;
+ }
+ });
+ return count;
+}
diff --git a/src/components/table/TableHeadCustom.tsx b/src/components/table/TableHeadCustom.tsx
new file mode 100644
index 0000000..dce1543
--- /dev/null
+++ b/src/components/table/TableHeadCustom.tsx
@@ -0,0 +1,96 @@
+// @mui
+import { Theme } from '@mui/material/styles';
+import {
+ Box,
+ SxProps,
+ Checkbox,
+ TableRow,
+ TableCell,
+ TableHead,
+ TableSortLabel,
+} from '@mui/material';
+
+// ----------------------------------------------------------------------
+
+const visuallyHidden = {
+ border: 0,
+ margin: -1,
+ padding: 0,
+ width: '1px',
+ height: '1px',
+ overflow: 'hidden',
+ position: 'absolute',
+ whiteSpace: 'nowrap',
+ clip: 'rect(0 0 0 0)',
+} as const;
+
+// ----------------------------------------------------------------------
+
+type Props = {
+ order?: 'asc' | 'desc';
+ orderBy?: string;
+ headLabel: any[];
+ rowCount?: number;
+ numSelected?: number;
+ onSort?: (id: string) => void;
+ onSelectAllRows?: (checked: boolean) => void;
+ sx?: SxProps;
+};
+
+export default function TableHeadCustom({
+ order,
+ orderBy,
+ rowCount = 0,
+ headLabel,
+ numSelected = 0,
+ onSort,
+ onSelectAllRows,
+ sx,
+}: Props) {
+ return (
+
+
+ {onSelectAllRows && (
+
+ 0 && numSelected < rowCount}
+ checked={rowCount > 0 && numSelected === rowCount}
+ onChange={(event: React.ChangeEvent) =>
+ onSelectAllRows(event.target.checked)
+ }
+ />
+
+ )}
+
+ {headLabel.map((headCell) => (
+
+ {onSort && headCell.needSort !== false ? (
+ onSort(headCell.id)}
+ sx={{ textTransform: 'capitalize' }}
+ >
+ {headCell.label}
+
+ {orderBy === headCell.id ? (
+
+ {order === 'desc' ? 'sorted descending' : 'sorted ascending'}
+
+ ) : null}
+
+ ) : (
+ headCell.label
+ )}
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/table/index.ts b/src/components/table/index.ts
new file mode 100644
index 0000000..8ef8635
--- /dev/null
+++ b/src/components/table/index.ts
@@ -0,0 +1 @@
+export { default as TableHeadCustom } from "./TableHeadCustom";
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 0000000..1076237
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,3 @@
+// API
+// ----------------------------------------------------------------------
+export const HOST_API = process.env.REACT_APP_HOST_API_KEY || "";
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx
new file mode 100644
index 0000000..c9bcdeb
--- /dev/null
+++ b/src/contexts/AuthContext.tsx
@@ -0,0 +1,126 @@
+import { HasChildren } from "@types";
+import { ResultCode } from "api";
+import { login as APILogin, logout as APILogout, me } from "api/auth";
+import { UserRole } from "codes/user";
+import useAPICall from "hooks/useAPICall";
+import { createContext, memo, useEffect, useMemo, useState } from "react";
+
+type Auth = {
+ initialized: boolean;
+
+ authenticated: boolean;
+
+ role: UserRole;
+ contractId: string | null;
+
+ login: (email: string, password: string) => Promise;
+ logout: VoidFunction;
+
+ changeContractId: (contractId: string) => Promise;
+
+ checkRole: (role?: UserRole) => boolean;
+};
+export const AuthContext = createContext({
+ initialized: false,
+
+ authenticated: false,
+
+ role: UserRole.NONE,
+ contractId: null,
+
+ login: async (email: string, password: string) => false,
+ logout: () => {},
+ changeContractId: async (contractId: string) => false,
+ checkRole: (role?: UserRole) => false,
+});
+
+type Props = HasChildren;
+function AuthContextProvider({ children }: Props) {
+ const [initialized, setInitialized] = useState(false);
+ const [role, setRole] = useState(UserRole.NONE);
+ const [contractId, setContractId] = useState(null);
+
+ const authenticated = useMemo(() => {
+ return role !== UserRole.NONE;
+ }, [role]);
+
+ const { callAPI: callMe } = useAPICall({
+ apiMethod: me,
+ onSuccess: (res) => {
+ setContractId(res.data.contract_id);
+ setRole(res.data.role);
+
+ setInitialized(true);
+ },
+ onFailed: () => {
+ clear();
+ setInitialized(true);
+ },
+ });
+
+ const { callAPI: callLogin } = useAPICall({
+ apiMethod: APILogin,
+ onSuccess: (res) => {
+ setContractId(res.data.contract_id);
+ setRole(res.data.role);
+ },
+ });
+
+ const { callAPI: callLogout } = useAPICall({
+ apiMethod: APILogout,
+ onSuccess: () => {
+ clear();
+ },
+ });
+
+ const clear = () => {
+ setRole(UserRole.NONE);
+ setContractId(null);
+ };
+
+ const login = async (email: string, password: string) => {
+ const res = await callLogin({ email, password });
+ if (!res) return false;
+
+ return res.result === ResultCode.SUCCESS;
+ };
+ const logout = () => {
+ callLogout({});
+ console.info("ログアウト");
+ };
+ const changeContractId = async (contractId: string) => {
+ console.error("未実装 成り代わり");
+ return false;
+ };
+
+ const checkRole = (targetRole?: UserRole): boolean => {
+ if (targetRole === undefined) return true;
+ return targetRole < role;
+ };
+
+ useEffect(() => {
+ callMe({});
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export default memo(AuthContextProvider);
diff --git a/src/contexts/DashboardLayoutContext.tsx b/src/contexts/DashboardLayoutContext.tsx
index ebc230d..b675995 100644
--- a/src/contexts/DashboardLayoutContext.tsx
+++ b/src/contexts/DashboardLayoutContext.tsx
@@ -1,6 +1,7 @@
import { HasChildren } from "@types";
-import { PageID } from "codes/page";
+import { PageID, TabID } from "codes/page";
import usePage from "hooks/usePage";
+import useResponsive from "hooks/useResponsive";
import useWindowSize from "hooks/useWindowSize";
import { ReactNode, createContext, useMemo, useState } from "react";
@@ -14,7 +15,12 @@ type ContextProps = {
tabs: ReactNode | null;
setTabs: (tabs: ReactNode | null) => void;
pageId: PageID;
- setPageId: (tabs: PageID) => void;
+ setPageId: (pageId: PageID) => void;
+ tabId: TabID;
+ setTabId: (tabId: TabID) => void;
+
+ showDrawer: boolean;
+ overBreakPoint: boolean;
};
const contextInit: ContextProps = {
headerTitle: "",
@@ -26,7 +32,12 @@ const contextInit: ContextProps = {
tabs: null,
setTabs: (tabs: ReactNode | null) => {},
pageId: PageID.NONE,
- setPageId: (tabs: PageID) => {},
+ setPageId: (pageId: PageID) => {},
+ tabId: TabID.NONE,
+ setTabId: (tabId: TabID) => {},
+
+ showDrawer: false,
+ overBreakPoint: false,
};
export const DashboardLayoutContext = createContext(contextInit);
@@ -38,11 +49,15 @@ export function DashboardLayoutContextProvider({ children }: Props) {
const [tabs, setTabs] = useState(null);
const { width: innerWidth, height: innerHeight } = useWindowSize();
- const { pageId, setPageId } = usePage();
+ const { pageId, setPageId, tabId, setTabId } = usePage();
+
+ const overBreakPoint = !!useResponsive("up", "sm");
+
+ const showDrawer = useMemo(() => overBreakPoint, [overBreakPoint]);
const contentsWidth = useMemo(() => {
- return innerWidth - drawerWidth;
- }, [drawerWidth, innerWidth]);
+ return innerWidth - (showDrawer ? drawerWidth : 0);
+ }, [drawerWidth, innerWidth, showDrawer]);
return (
{children}
diff --git a/src/contexts/PageContext.tsx b/src/contexts/PageContext.tsx
index f928cbf..74a326b 100644
--- a/src/contexts/PageContext.tsx
+++ b/src/contexts/PageContext.tsx
@@ -1,27 +1,33 @@
import { HasChildren } from "@types";
-import { PageID } from "codes/page";
-import useWindowSize from "hooks/useWindowSize";
-import { ReactNode, createContext, useMemo, useState } from "react";
+import { PageID, TabID } from "codes/page";
+import { createContext, useState } from "react";
type ContextProps = {
pageId: PageID;
+ tabId: TabID;
setPageId: (pageId: PageID) => void;
+ setTabId: (tabId: TabID) => void;
};
const contextInit: ContextProps = {
pageId: PageID.NONE,
+ tabId: TabID.NONE,
setPageId: (pageId: PageID) => {},
+ setTabId: (tabId: TabID) => {},
};
export const PageContext = createContext(contextInit);
type Props = HasChildren;
export function PageContextProvider({ children }: Props) {
const [pageId, setPageId] = useState(PageID.NONE);
+ const [tabId, setTabId] = useState(TabID.NONE);
return (
{children}
diff --git a/src/contexts/SearchConditionContext.tsx b/src/contexts/SearchConditionContext.tsx
new file mode 100644
index 0000000..10770c4
--- /dev/null
+++ b/src/contexts/SearchConditionContext.tsx
@@ -0,0 +1,131 @@
+import { cloneDeep, isEqual, unset } from "lodash";
+import { ReactNode, createContext, useEffect, useMemo, useState } from "react";
+// form
+import { useLocation } from "react-router";
+import { Dictionary } from "@types";
+import useNavigateCustom from "hooks/useNavigateCustom";
+
+// ----------------------------------------------------------------------
+
+const _condition: Dictionary = {};
+
+const initialState = {
+ initialized: false,
+ condition: _condition,
+ get: (key: string): string => "",
+ initializeCondition: () => {},
+ addCondition: (condition: Dictionary) => {},
+ clearCondition: () => {},
+};
+
+export const SearchConditionContext = createContext(initialState);
+type Props = {
+ children: ReactNode;
+};
+
+export function SearchConditionContextProvider({ children }: Props) {
+ const [condition, _setCondition] = useState({});
+ const [initialized, setInitialized] = useState(false);
+
+ const { pathname, search } = useLocation();
+ const { navigateWhenChanged } = useNavigateCustom();
+
+ const setCondition = (after: Dictionary, message?: any) => {
+ if (message) {
+ console.log("Contidion Change", { after, message });
+ }
+ _setCondition(after);
+ };
+
+ const initializeCondition = () => {
+ const after: Dictionary = {};
+ const urlParam = new URLSearchParams(search);
+ for (const [key, value] of urlParam.entries()) {
+ after[key] = value;
+ }
+
+ if (!isEqual(after, condition)) {
+ console.log("initialCondition", { before: condition, after });
+ setCondition(after, "initializeCondition");
+ }
+ setInitialized(true);
+ };
+
+ const get = (key: string) => {
+ return condition[key] ?? "";
+ };
+
+ const getCondition = useMemo(() => {
+ return cloneDeep(condition);
+ }, [condition]);
+
+ const addCondition = (additional: Dictionary) => {
+ if (!initialized) return;
+ const before = cloneDeep(condition);
+ const after = cloneDeep(condition);
+
+ Object.keys(additional).forEach((key) => {
+ unset(after, key);
+ if (additional[key] !== "") {
+ after[key] = additional[key];
+ }
+ });
+
+ if (!isEqual(before, after)) {
+ console.log("addCondition", { additional, before, after });
+ setCondition(after, "addCondition");
+ }
+ };
+
+ const searchParams = useMemo(() => {
+ const params = new URLSearchParams();
+ if (!initialized) return params;
+
+ Object.keys(condition).forEach((key) => {
+ params.append(key, condition[key]);
+ });
+ params.sort();
+ return params;
+ }, [condition]);
+
+ const applyToURL = () => {
+ if (!initialized) return;
+ const params = searchParams;
+ const searchStr = params.toString();
+ const url = pathname + (searchStr ? "?" + searchStr : "");
+
+ navigateWhenChanged(pathname, condition, "applyToURL");
+ };
+
+ const clearCondition = () => {
+ setCondition({}, "clearCondition");
+ setInitialized(false);
+ console.log("clearCondition");
+ };
+
+ useEffect(() => {
+ if (initialized) {
+ console.log("call applyToURL", { condition, initialized });
+ applyToURL();
+ }
+ }, [condition, initialized]);
+
+ useEffect(() => {
+ initializeCondition();
+ }, [pathname, search]);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/hooks/useAPICall.ts b/src/hooks/useAPICall.ts
new file mode 100644
index 0000000..b6e7ec6
--- /dev/null
+++ b/src/hooks/useAPICall.ts
@@ -0,0 +1,168 @@
+import { useCallback, useMemo, useState } from "react";
+import { APICommonResponse, apiRequest, ResultCode } from "api";
+import { UseFormReturn } from "react-hook-form";
+import { Dictionary } from "@types";
+import { useSnackbar } from "notistack";
+
+export const APIErrorType = {
+ NONE: "none",
+ INPUT: "input",
+ SERVER: "server",
+ EXCLUSIVE: "exclusive",
+} as const;
+export type APIErrorType = (typeof APIErrorType)[keyof typeof APIErrorType];
+
+export default function useAPICall<
+ T extends object,
+ U extends APICommonResponse
+>({
+ apiMethod,
+ onSuccess,
+ onFailed,
+ form,
+ successMessage = false,
+ failedMessage = false,
+}: {
+ apiMethod: (sendData: T) => Promise;
+ onSuccess?: (res: U, sendData: T) => void;
+ onFailed?: (res: APICommonResponse | null) => void;
+ form?: UseFormReturn;
+ successMessage?: ((res: U) => string) | boolean;
+ failedMessage?: ((res: APICommonResponse | null) => string) | boolean;
+}) {
+ const [sending, setSending] = useState(false);
+ const [sendData, setSendData] = useState(null);
+ const [result, setResult] = useState(null);
+ const [errors, setErrors] = useState({});
+ const [validationError, setValidationError] = useState(false);
+ const [exclusiveError, setExclusiveError] = useState(false);
+ const [generalErrorMessage, setGeneralErrorMessage] = useState("");
+
+ const { enqueueSnackbar } = useSnackbar();
+
+ const clearErrors = () => {
+ setResult(null);
+ setErrors({});
+ setSendData(null);
+ setValidationError(false);
+ setExclusiveError(false);
+ setGeneralErrorMessage("");
+ };
+
+ // 入力項目に対してエラーレスポンスがあるか、ないかでエラーのタイプを設定する
+ const errorMode = useMemo(() => {
+ if (generalErrorMessage !== "") return APIErrorType.INPUT;
+ if (exclusiveError) return APIErrorType.EXCLUSIVE;
+ if (validationError) return APIErrorType.INPUT;
+ if (result === null) return APIErrorType.NONE;
+ if (result === ResultCode.SUCCESS) return APIErrorType.NONE;
+ if (sendData === null) return APIErrorType.NONE;
+
+ const messageKeys = Object.keys(errors);
+ if (messageKeys.length === 0) return APIErrorType.SERVER;
+
+ if (sendData) {
+ const sendDataKeys = Object.keys(sendData);
+ const find = messageKeys.find((key: string) => {
+ return sendDataKeys.includes(key);
+ });
+ if (find) {
+ return APIErrorType.INPUT;
+ }
+ }
+ return APIErrorType.SERVER;
+ }, [result, errors, validationError, exclusiveError, generalErrorMessage]);
+
+ const getSuccessMessageStr = useCallback(
+ (res: U) => {
+ if (typeof successMessage === "boolean") {
+ return successMessage ? "成功しました" : "";
+ }
+ return successMessage(res);
+ },
+ [successMessage]
+ );
+
+ const getFailedMessageStr = useCallback(
+ (res: APICommonResponse | null) => {
+ if (typeof failedMessage === "boolean") {
+ return failedMessage ? "失敗しました" : "";
+ }
+ return failedMessage(res);
+ },
+ [failedMessage]
+ );
+
+ const successCallback = (res: U, sendData: T) => {
+ setResult(ResultCode.SUCCESS);
+ const message = getSuccessMessageStr(res);
+ if (message) {
+ enqueueSnackbar(message);
+ }
+ if (onSuccess) {
+ onSuccess(res, sendData);
+ }
+ };
+
+ const failedCallback = (res: APICommonResponse | null) => {
+ if (res?.result === ResultCode.EXCLUSIVE_ERROR) {
+ setExclusiveError(true);
+ }
+ if (res) {
+ setResult(res.result);
+ }
+
+ if (res?.messages.errors) {
+ console.log("seterrors", res.messages.errors);
+ setErrors(res.messages.errors);
+ }
+ if (res?.messages.general) {
+ setGeneralErrorMessage(res.messages.general);
+ }
+
+ const message = getFailedMessageStr(res);
+ if (message) {
+ enqueueSnackbar(message, { variant: "error" });
+ }
+ if (onFailed) {
+ onFailed(res);
+ }
+ };
+
+ const handleValidationError = (error: any) => {
+ console.log(error);
+ setValidationError(true);
+ };
+
+ const callAPI = async (sendData: T) => {
+ if (form) {
+ form.clearErrors();
+ }
+ clearErrors();
+
+ setSendData(sendData);
+ const res = await apiRequest({
+ apiMethod,
+ onSuccess: successCallback,
+ onFailed: failedCallback,
+ sendData,
+ setSending,
+ errorSetter: form?.setError,
+ });
+
+ return res;
+ };
+
+ const makeSendData = (data: T) => {
+ return data;
+ };
+
+ return {
+ callAPI,
+ sending,
+ errorMode,
+ generalErrorMessage,
+ handleValidationError,
+ makeSendData,
+ };
+}
diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts
new file mode 100644
index 0000000..5397b45
--- /dev/null
+++ b/src/hooks/useAuth.ts
@@ -0,0 +1,7 @@
+import { AuthContext } from "contexts/AuthContext";
+import { useContext } from "react";
+
+export default function useAuth() {
+ const context = useContext(AuthContext);
+ return context;
+}
diff --git a/src/hooks/useDashBoard.ts b/src/hooks/useDashBoard.ts
index dc1ef74..d3a720b 100644
--- a/src/hooks/useDashBoard.ts
+++ b/src/hooks/useDashBoard.ts
@@ -1,14 +1,16 @@
import { useContext, useEffect } from "react";
import { DashboardLayoutContext } from "contexts/DashboardLayoutContext";
-import { PageID } from "codes/page";
+import { PageID, TabID } from "codes/page";
-export default function useDashboard(pageId?: PageID) {
+export default function useDashboard(pageId?: PageID, tabId?: TabID) {
const context = useContext(DashboardLayoutContext);
useEffect(() => {
if (pageId) {
- context.setPageId(pageId ?? PageID.NONE);
- // console.log("CURRENT", pageId);
+ context.setPageId(pageId);
+ }
+ if (tabId) {
+ context.setTabId(tabId);
}
}, []);
diff --git a/src/hooks/useNavigateCustom.ts b/src/hooks/useNavigateCustom.ts
index 714ee95..0d1b781 100644
--- a/src/hooks/useNavigateCustom.ts
+++ b/src/hooks/useNavigateCustom.ts
@@ -1,5 +1,6 @@
import { useLocation, useNavigate } from "react-router";
import { Dictionary } from "@types";
+import { PageID } from "codes/page";
export default function useNavigateCustom() {
const navigate = useNavigate();
diff --git a/src/hooks/useSearchConditionContext.ts b/src/hooks/useSearchConditionContext.ts
new file mode 100644
index 0000000..54de11f
--- /dev/null
+++ b/src/hooks/useSearchConditionContext.ts
@@ -0,0 +1,6 @@
+import { useContext } from "react";
+import { SearchConditionContext } from "contexts/SearchConditionContext";
+
+export default function useSearchConditionContext() {
+ return useContext(SearchConditionContext);
+}
diff --git a/src/hooks/useSnackbarCustom.ts b/src/hooks/useSnackbarCustom.ts
new file mode 100644
index 0000000..12b39cb
--- /dev/null
+++ b/src/hooks/useSnackbarCustom.ts
@@ -0,0 +1,26 @@
+import { OptionsObject, useSnackbar } from "notistack";
+
+export default function useSnackbarCustom() {
+ const { enqueueSnackbar } = useSnackbar();
+
+ const info = (message: string, option?: OptionsObject) => {
+ enqueueSnackbar(message, { variant: "info", ...option });
+ };
+ const success = (message: string, option?: OptionsObject) => {
+ enqueueSnackbar(message, { variant: "success", ...option });
+ };
+ const warn = (message: string, option?: OptionsObject) => {
+ enqueueSnackbar(message, { variant: "warning", ...option });
+ };
+ const error = (message: string, option?: OptionsObject) => {
+ enqueueSnackbar(message, { variant: "error", ...option });
+ };
+
+ return {
+ enqueueSnackbar,
+ info,
+ success,
+ warn,
+ error,
+ };
+}
diff --git a/src/hooks/useTable.ts b/src/hooks/useTable.ts
new file mode 100644
index 0000000..2dd6860
--- /dev/null
+++ b/src/hooks/useTable.ts
@@ -0,0 +1,177 @@
+import { ceil, max } from "lodash";
+import { useEffect, useMemo, useState } from "react";
+import { useParams } from "react-router";
+import useNavigateCustom from "./useNavigateCustom";
+import useSearchConditionContext from "./useSearchConditionContext";
+import useURLSearchParams from "./useURLSearchParams";
+import usePage from "./usePage";
+import { getListPagePath } from "routes/path";
+
+// ----------------------------------------------------------------------
+
+export type UseTableReturn = {
+ order: "asc" | "desc";
+ page: number;
+ sort: string;
+ rowsPerPage: number;
+ fetched: boolean;
+ row: T[];
+ fillteredRow: T[];
+ isNotFound: boolean;
+ dataLength: number;
+ //
+ onSort: (id: string) => void;
+ onChangePage: (event: unknown, page: number) => void;
+ onChangeRowsPerPage: (event: React.ChangeEvent) => void;
+ //
+ setRowData: (data: T[]) => void;
+ //
+ ROWS_PER_PAGES: number[];
+};
+
+export default function useTable(): UseTableReturn {
+ const ROWS_PER_PAGES = [20, 50, 100];
+
+ const ORDER = "order";
+ const SORT = "sort";
+ const ROWS = "rows";
+
+ const { pageId } = usePage();
+
+ const { page: urlPage } = useParams();
+
+ const { navigateWhenChanged } = useNavigateCustom();
+
+ const { addCondition, initialized, get } = useSearchConditionContext();
+
+ const { search } = useURLSearchParams();
+
+ const [sort, setSort] = useState("");
+
+ const [order, setOrder] = useState<"asc" | "desc">("asc");
+
+ const [rowsPerPage, setRowsPerPage] = useState(ROWS_PER_PAGES[0]);
+
+ const [page, setPage] = useState(0);
+
+ const [data, setData] = useState([]);
+
+ const [fetched, setFetched] = useState(false);
+
+ const [requestPage, setRequestPage] = useState(null);
+
+ const paging = (page: number) => {
+ return getListPagePath(pageId, page);
+ };
+
+ // 表示する行
+ const fillteredRow = useMemo(() => {
+ if (requestPage !== page) {
+ // console.log("fillteredRow DIFFs", { requestPage, page });
+ return [];
+ }
+ return data.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
+ }, [page, requestPage, rowsPerPage, data]);
+
+ const isNotFound = useMemo(() => {
+ if (data.length !== 0) return false;
+ if (!fetched) return false;
+ return true;
+ }, [fetched, data]);
+
+ const dataLength = useMemo(() => {
+ return data.length;
+ }, [data]);
+
+ const maxPage = useMemo(() => {
+ return max([ceil(dataLength / rowsPerPage) - 1, 0]) ?? 0;
+ }, [dataLength, rowsPerPage]);
+
+ const onSort = (id: string) => {
+ const isAsc = sort === id && order === "asc";
+ if (id !== "") {
+ const tmpOrder = isAsc ? "desc" : "asc";
+ setOrder(tmpOrder);
+ setSort(id);
+
+ addCondition({
+ [SORT]: id,
+ [ORDER]: tmpOrder,
+ });
+ }
+ };
+
+ const onChangePage = (event: unknown, page: number) => {
+ setRequestPage(page);
+ };
+
+ const onChangeRowsPerPage = (event: React.ChangeEvent) => {
+ const newRowsPerPage = parseInt(event.target.value, 10);
+ setRowsPerPage(newRowsPerPage);
+ setPage(0);
+ addCondition({
+ rows: String(newRowsPerPage),
+ });
+ };
+
+ // 一度ページを0にする
+ // 下記のページ遷移制御によって、requestPageのと調整を行う
+ const setRowData = (data: T[]) => {
+ setPage(0);
+ setData(data);
+ setFetched(true);
+ };
+
+ // 主にページ遷移を制御する
+ // リストデータ数や1ページあたりの表示数によっては、現在のページが最大ページを超えてしまう可能性があるので
+ // 一度、requestPageにページ番号をためておき、最大ページを超えないように調整する
+ useEffect(() => {
+ if (requestPage !== null && fetched) {
+ const newPage = requestPage <= maxPage ? requestPage : maxPage;
+ if (page !== newPage) {
+ setPage(newPage);
+ }
+ if (requestPage !== newPage) {
+ setRequestPage(newPage);
+ }
+ navigateWhenChanged(paging(newPage), search, "useTable.paging");
+ }
+ }, [requestPage, maxPage, fetched, page]);
+
+ // クエリパラメータから各初期値を設定する
+ useEffect(() => {
+ if (initialized) {
+ setSort(get(SORT));
+ const order = get(ORDER);
+ if (order === "asc" || order === "desc") {
+ setOrder(order);
+ }
+
+ const rows = Number(get(ROWS));
+ setRowsPerPage(ROWS_PER_PAGES.includes(rows) ? rows : ROWS_PER_PAGES[0]);
+ }
+ }, [initialized]);
+ useEffect(() => {
+ setRequestPage(Number(urlPage ?? "0"));
+ }, [urlPage]);
+
+ return {
+ order,
+ page,
+ sort,
+ rowsPerPage,
+ fetched,
+ row: data,
+ fillteredRow,
+ isNotFound,
+ dataLength,
+ //
+ onSort,
+ onChangePage,
+ onChangeRowsPerPage,
+ //
+ setRowData,
+ //
+ ROWS_PER_PAGES,
+ };
+}
diff --git a/src/hooks/useURLSearchParams.ts b/src/hooks/useURLSearchParams.ts
new file mode 100644
index 0000000..e782161
--- /dev/null
+++ b/src/hooks/useURLSearchParams.ts
@@ -0,0 +1,48 @@
+import { useEffect, useState } from 'react';
+import { useLocation, useNavigate } from 'react-router';
+
+export default function useURLSearchParams() {
+ const navigate = useNavigate();
+ const { pathname, search } = useLocation();
+
+ const [urlParam, setUrlParam] = useState(new URLSearchParams(search));
+ const [needApply, setNeedApply] = useState(false);
+
+ const setParam = () => {
+ const path = pathname;
+ const url = path + '?' + urlParam.toString();
+
+ const current = pathname + search;
+ if (url !== current) {
+ navigate(url);
+ }
+ setNeedApply(false);
+ };
+
+ const appendAll = (list: { key: string; value: string | null | undefined }[]) => {
+ list.forEach(({ key, value }) => {
+ urlParam.delete(key);
+ if (value) {
+ urlParam.append(key, value);
+ }
+ });
+ urlParam.sort();
+ setNeedApply(true);
+ };
+
+ useEffect(() => {
+ setUrlParam(new URLSearchParams(search));
+ }, [search]);
+
+ useEffect(() => {
+ if (needApply) {
+ setParam();
+ }
+ }, [needApply]);
+
+ return {
+ search,
+ urlParam,
+ appendAll,
+ };
+}
diff --git a/src/index.tsx b/src/index.tsx
index 032464f..a4eefe0 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,16 +1,15 @@
-import React from 'react';
-import ReactDOM from 'react-dom/client';
-import './index.css';
-import App from './App';
-import reportWebVitals from './reportWebVitals';
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import "./index.css";
+import reportWebVitals from "./reportWebVitals";
const root = ReactDOM.createRoot(
- document.getElementById('root') as HTMLElement
+ document.getElementById("root") as HTMLElement
);
root.render(
-
-
-
+ //
+
+ //
);
// If you want to start measuring performance in your app, pass a function
diff --git a/src/layouts/dashbord/index.tsx b/src/layouts/dashbord/index.tsx
index 4457ebc..2c0d946 100644
--- a/src/layouts/dashbord/index.tsx
+++ b/src/layouts/dashbord/index.tsx
@@ -23,9 +23,7 @@ function Copyright() {
function App() {
const [mobileOpen, setMobileOpen] = useState(false);
- const isSmUp = useResponsive("up", "sm");
-
- const { drawerWidth, innerHeight, innerWidth, contentsWidth } =
+ const { drawerWidth, innerHeight, innerWidth, contentsWidth, showDrawer } =
useDashboard();
const handleDrawerToggle = () => {
@@ -34,7 +32,7 @@ function App() {
useEffect(() => {
console.log({ drawerWidth, innerWidth, contentsWidth });
- }, []);
+ }, [drawerWidth, innerWidth, contentsWidth]);
return (
@@ -42,7 +40,7 @@ function App() {
component="nav"
sx={{ width: { sm: drawerWidth }, flexShrink: { md: 0 } }}
>
- {isSmUp ? null : (
+ {showDrawer && (
-
+
diff --git a/src/layouts/dashbord/navigator.tsx b/src/layouts/dashbord/navigator.tsx
index f76ffb6..ad8ee06 100644
--- a/src/layouts/dashbord/navigator.tsx
+++ b/src/layouts/dashbord/navigator.tsx
@@ -1,34 +1,28 @@
-import * as React from "react";
+import { ExpandLess, ExpandMore } from "@mui/icons-material";
+import HomeIcon from "@mui/icons-material/Home";
+import PeopleIcon from "@mui/icons-material/People";
+import SettingsIcon from "@mui/icons-material/Settings";
+import { Collapse } from "@mui/material";
+import Box from "@mui/material/Box";
import Divider from "@mui/material/Divider";
import Drawer, { DrawerProps } from "@mui/material/Drawer";
import List from "@mui/material/List";
-import Box from "@mui/material/Box";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
-import HomeIcon from "@mui/icons-material/Home";
-import PeopleIcon from "@mui/icons-material/People";
-import DnsRoundedIcon from "@mui/icons-material/DnsRounded";
-import PermMediaOutlinedIcon from "@mui/icons-material/PhotoSizeSelectActual";
-import PublicIcon from "@mui/icons-material/Public";
-import SettingsEthernetIcon from "@mui/icons-material/SettingsEthernet";
-import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
-import TimerIcon from "@mui/icons-material/Timer";
-import SettingsIcon from "@mui/icons-material/Settings";
-import PhonelinkSetupIcon from "@mui/icons-material/PhonelinkSetup";
-import { UserRole } from "codes/user";
-import { useLocation } from "react-router-dom";
-import { PATH, getPath } from "routes/path";
-import usePage from "hooks/usePage";
import { PageID } from "codes/page";
-import { ExpandLess, ExpandMore } from "@mui/icons-material";
-import { Collapse } from "@mui/material";
+import { UserRole } from "codes/user";
+import useAuth from "hooks/useAuth";
import useNavigateCustom from "hooks/useNavigateCustom";
+import usePage from "hooks/usePage";
+import * as React from "react";
+import { getPath } from "routes/path";
type Group = {
label: string;
children: SubGroup[];
+ role?: UserRole;
};
type SubGroup = {
@@ -36,7 +30,7 @@ type SubGroup = {
icon: React.ReactNode;
children?: Child[];
- // 子要素を持たない場合は下記は必須
+ // 子要素を持たない場合は設定
id?: PageID;
role?: UserRole;
};
@@ -54,6 +48,7 @@ const categories: Group[] = [
{
label: "契約",
icon: ,
+ role: UserRole.SUPER_ADMIN,
children: [
{
id: PageID.DASHBOARD_CONTRACT_LIST,
@@ -65,24 +60,14 @@ const categories: Group[] = [
},
],
},
- // { id: "Database", icon: , navigate: "contract" },
- // { id: "Storage", icon: },
- // { id: "Hosting", icon: },
- // { id: "Functions", icon: },
- // {
- // id: "Machine learning",
- // icon: ,
- // },
],
},
- // {
- // id: "Quality",
- // children: [
- // { id: "Analytics", icon: },
- // { id: "Performance", icon: },
- // { id: "Test Lab", icon: },
- // ],
- // },
+ {
+ label: "アカウント",
+ children: [
+ { label: "ログアウト", icon: , id: PageID.LOGOUT },
+ ],
+ },
];
const item = {
@@ -129,6 +114,9 @@ export default function Navigator(props: DrawerProps) {
function Group(group: Group) {
const { label, children } = group;
+ const { checkRole } = useAuth();
+ if (!checkRole(group.role)) return null;
+
return (
@@ -150,15 +138,20 @@ function SubGroup({ icon, label, id, children, role }: SubGroup) {
const [open, setOpen] = React.useState(false);
+ const { checkRole } = useAuth();
+
React.useEffect(() => {
setOpen(shouldOpen);
}, [shouldOpen]);
+ if (!checkRole(role)) return null;
+
// 子要素ありの場合
if (elements && elements.length !== 0) {
const handleClick = () => {
setOpen(!open);
};
+
return (
<>
@@ -184,7 +177,7 @@ function SubGroup({ icon, label, id, children, role }: SubGroup) {
};
const selected = id === pageId;
return (
-
+
{icon}
{label}
@@ -194,12 +187,15 @@ function SubGroup({ icon, label, id, children, role }: SubGroup) {
function useContents(children: Child[]) {
const { pageId } = usePage();
const { navigateWhenChanged } = useNavigateCustom();
+ const { checkRole } = useAuth();
const [shouldOpen, setShouldOpen] = React.useState(false);
const elements = React.useMemo(() => {
setShouldOpen(false);
- return children.map(({ label, id }, index) => {
+ return children.map(({ label, id, role }, index) => {
+ if (!checkRole(role)) return;
+
const selected = id === pageId;
if (selected) {
setShouldOpen(true);
diff --git a/src/layouts/dashbord/tab/ContractTabs.tsx b/src/layouts/dashbord/tab/ContractTabs.tsx
index 22df351..9dd233e 100644
--- a/src/layouts/dashbord/tab/ContractTabs.tsx
+++ b/src/layouts/dashbord/tab/ContractTabs.tsx
@@ -1,16 +1,16 @@
import { Tabs } from "@mui/material";
import { TabProps, useTab } from "./tabutil";
-import { PageID } from "codes/page";
+import { PageID, TabID } from "codes/page";
import { getPath } from "routes/path";
const tabs: TabProps[] = [
{
label: "一覧",
- pageId: PageID.DASHBOARD_CONTRACT_LIST,
+ tabId: TabID.NONE,
},
{
label: "詳細",
- pageId: PageID.DASHBOARD_CONTRACT_DETAIL,
+ tabId: TabID.A,
},
];
diff --git a/src/layouts/dashbord/tab/tabutil.tsx b/src/layouts/dashbord/tab/tabutil.tsx
index b8735fa..f98864f 100644
--- a/src/layouts/dashbord/tab/tabutil.tsx
+++ b/src/layouts/dashbord/tab/tabutil.tsx
@@ -1,4 +1,4 @@
-import { PageID } from "codes/page";
+import { PageID, TabID } from "codes/page";
import usePage from "hooks/usePage";
import { useMemo } from "react";
import { Tab } from ".";
@@ -6,15 +6,15 @@ import { getPath } from "routes/path";
export type TabProps = {
label: string;
- pageId: PageID;
+ tabId: TabID;
};
export function useTab(tabs: TabProps[]) {
- const { pageId } = usePage();
+ const { pageId, tabId } = usePage();
const elements = useMemo(() => {
- return tabs.map(({ label, pageId: elementPageId }, index) => {
- const path = getPath(elementPageId);
+ return tabs.map(({ label, tabId: elementTabId }, index) => {
+ const path = getPath([pageId, tabId]);
return ;
});
}, [tabs]);
@@ -22,10 +22,10 @@ export function useTab(tabs: TabProps[]) {
const getTabIndex = useMemo(() => {
return (
tabs.findIndex((tab) => {
- return tab.pageId === pageId;
+ return tab.tabId === tabId;
}) ?? 0
);
- }, [pageId, tabs]);
+ }, [tabId, tabs]);
return {
elements,
diff --git a/src/layouts/simple/index.tsx b/src/layouts/simple/index.tsx
new file mode 100644
index 0000000..7b1826c
--- /dev/null
+++ b/src/layouts/simple/index.tsx
@@ -0,0 +1,26 @@
+import { AppBar, Box, Grid } from "@mui/material";
+import { Outlet } from "react-router-dom";
+
+export default function SimpleLayout() {
+ return (
+
+
+
+
+
+ EasyReceipt
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx
new file mode 100644
index 0000000..5d3184a
--- /dev/null
+++ b/src/pages/auth/login.tsx
@@ -0,0 +1,80 @@
+import { yupResolver } from "@hookform/resolvers/yup";
+import { LoadingButton } from "@mui/lab";
+import { AppBar, Box, Grid, Stack, Typography } from "@mui/material";
+import { PageID } from "codes/page";
+import InputAlert from "components/form/InputAlert";
+import { FormProvider, RHFTextField } from "components/hook-form";
+import useAuth from "hooks/useAuth";
+import useNavigateCustom from "hooks/useNavigateCustom";
+import useSnackbarCustom from "hooks/useSnackbarCustom";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { getPath } from "routes/path";
+import * as Yup from "yup";
+
+type FormProps = {
+ email: string;
+ password: string;
+};
+
+const LoginSchema = Yup.object().shape({
+ email: Yup.string().required("必須項目です"),
+ password: Yup.string().required("必須項目です"),
+});
+
+export default function Login() {
+ const { success, error } = useSnackbarCustom();
+
+ const [message, setMessage] = useState("");
+ const [sending, setSending] = useState(false);
+
+ const { login } = useAuth();
+
+ const { navigateWhenChanged } = useNavigateCustom();
+
+ const form = useForm({
+ defaultValues: {
+ email: "",
+ password: "",
+ },
+ resolver: yupResolver(LoginSchema),
+ });
+
+ const handleSubmit = async (data: FormProps) => {
+ setMessage("");
+ setSending(true);
+ const ret = await login(data.email, data.password);
+
+ setSending(false);
+
+ if (ret) {
+ success("ログイン成功");
+ navigateWhenChanged(getPath(PageID.DASHBOARD_CONTRACT_LIST));
+ } else {
+ error("ログイン失敗");
+ setMessage("入力情報を確認してください");
+ }
+ };
+
+ return (
+
+
+
+ ログイン
+
+
+
+
+
+ ログイン
+
+
+
+
+ );
+}
diff --git a/src/pages/auth/logout.tsx b/src/pages/auth/logout.tsx
new file mode 100644
index 0000000..5817c7d
--- /dev/null
+++ b/src/pages/auth/logout.tsx
@@ -0,0 +1,21 @@
+import { PageID } from "codes/page";
+import useAuth from "hooks/useAuth";
+import useNavigateCustom from "hooks/useNavigateCustom";
+import useSnackbarCustom from "hooks/useSnackbarCustom";
+import { useEffect } from "react";
+import { getPath } from "routes/path";
+
+export default function Logout() {
+ const { logout } = useAuth();
+ const { info, success } = useSnackbarCustom();
+
+ const { navigateWhenChanged } = useNavigateCustom();
+
+ useEffect(() => {
+ logout();
+ info("ログアウトしました");
+ navigateWhenChanged(getPath(PageID.LOGIN));
+ }, []);
+
+ return null;
+}
diff --git a/src/pages/dashboard/contract/detail.tsx b/src/pages/dashboard/contract/detail.tsx
index cf97736..a165367 100644
--- a/src/pages/dashboard/contract/detail.tsx
+++ b/src/pages/dashboard/contract/detail.tsx
@@ -1,12 +1,13 @@
import { Box } from "@mui/material";
-import { PageID } from "codes/page";
+import { PageID, TabID } from "codes/page";
import useDashboard from "hooks/useDashBoard";
import ContractTabs from "layouts/dashbord/tab/ContractTabs";
import { useEffect } from "react";
export default function ContractDetail() {
const { setHeaderTitle, setTabs } = useDashboard(
- PageID.DASHBOARD_CONTRACT_DETAIL
+ PageID.DASHBOARD_CONTRACT_DETAIL,
+ TabID.A
);
useEffect(() => {
diff --git a/src/pages/dashboard/contract/list.tsx b/src/pages/dashboard/contract/list.tsx
index 752d083..90b0755 100644
--- a/src/pages/dashboard/contract/list.tsx
+++ b/src/pages/dashboard/contract/list.tsx
@@ -1,17 +1,188 @@
-import { Box } from "@mui/material";
-import { PageID } from "codes/page";
+import {
+ Box,
+ Grid,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TablePagination,
+ TableRow,
+ TextField,
+} from "@mui/material";
+import { PageID, TabID } from "codes/page";
+import { TableHeadCustom } from "components/table";
+import { SearchConditionContextProvider } from "contexts/SearchConditionContext";
import useDashboard from "hooks/useDashBoard";
+import useTable, { UseTableReturn } from "hooks/useTable";
import ContractTabs from "layouts/dashbord/tab/ContractTabs";
import { useEffect } from "react";
+import { Contract } from "types/contract";
export default function ContractList() {
const { setHeaderTitle, setTabs } = useDashboard(
- PageID.DASHBOARD_CONTRACT_LIST
+ PageID.DASHBOARD_CONTRACT_LIST,
+ TabID.NONE
);
+ const table = useTable();
+
useEffect(() => {
setHeaderTitle("契約者一覧");
setTabs();
}, []);
- return ContractList;
+
+ return (
+
+
+
+ );
+}
+
+type CommonProps = {
+ table: UseTableReturn;
+};
+function Page({ table }: CommonProps) {
+ const {
+ order,
+ page,
+ sort,
+ rowsPerPage,
+ fetched,
+ fillteredRow,
+ isNotFound,
+ dataLength,
+ //
+ onSort,
+ onChangePage,
+ onChangeRowsPerPage,
+ //
+ setRowData,
+ //
+ ROWS_PER_PAGES,
+ } = table;
+ return (
+
+
+
+
+ );
+}
+
+function SearchBox({ table }: CommonProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function TableBox({ table }: CommonProps) {
+ const TABLE_HEAD = [
+ { id: "id", label: "ID", align: "left" },
+ { id: "name", label: "名前", align: "left" },
+ { id: "emply", label: "---", align: "left" },
+ ];
+ const {
+ order,
+ page,
+ sort,
+ rowsPerPage,
+ fetched,
+ fillteredRow,
+ isNotFound,
+ dataLength,
+ //
+ onSort,
+ onChangePage,
+ onChangeRowsPerPage,
+ //
+ setRowData,
+ //
+ ROWS_PER_PAGES,
+ } = table;
+
+ useEffect(() => {
+ setRowData([
+ { id: "iwabuchi", name: "hei" },
+ { id: "iwabuchi", name: "hei1" },
+ ]);
+ }, []);
+
+ return (
+ <>
+
+
+
+
+
+ {fillteredRow.map((row, index) => (
+
+ ))}
+
+
+
+
+
+
+
+ >
+ );
+}
+
+type RowProps = {
+ data: Contract;
+};
+function Row({ data }: RowProps) {
+ return (
+
+ {data.id}
+ {data.name}
+ {data.created_at}
+
+ );
}
diff --git a/src/providers/CsrfTokenProvider.tsx b/src/providers/CsrfTokenProvider.tsx
new file mode 100644
index 0000000..1b208ae
--- /dev/null
+++ b/src/providers/CsrfTokenProvider.tsx
@@ -0,0 +1,17 @@
+import { csrfToken } from "api/auth";
+import { memo, useEffect, useState } from "react";
+
+function CsrfTokenProvider() {
+ const [done, setDone] = useState(false);
+
+ useEffect(() => {
+ if (!done) {
+ setDone(true);
+ csrfToken();
+ }
+ }, []);
+
+ return null;
+}
+
+export default memo(CsrfTokenProvider);
diff --git a/src/providers/SnackbarProvider.tsx b/src/providers/SnackbarProvider.tsx
new file mode 100644
index 0000000..ca09d35
--- /dev/null
+++ b/src/providers/SnackbarProvider.tsx
@@ -0,0 +1,9 @@
+import { HasChildren } from "@types";
+import { SnackbarProvider as NotistackProvider } from "notistack";
+
+type Props = HasChildren;
+export default function SnackbarProvider({ children }: Props) {
+ return (
+ {children}
+ );
+}
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 3678cd2..f1ac201 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -1,9 +1,10 @@
import LoadingScreen from "components/LoadingScreen";
import DashboardLayout from "layouts/dashbord";
import { ElementType, Suspense, lazy } from "react";
-import { useLocation, useRoutes } from "react-router-dom";
+import { RouteObject, useLocation, useRoutes } from "react-router-dom";
import { PATH, getRoute } from "./path";
import { PageID } from "codes/page";
+import SimpleLayout from "layouts/simple";
const Loadable = (Component: ElementType) => (props: any) => {
return (
@@ -13,39 +14,49 @@ const Loadable = (Component: ElementType) => (props: any) => {
);
};
-export function Routes() {
- return useRoutes([
+const AuthRoutes = (): RouteObject => ({
+ element: ,
+ children: [
{
- path: "testa",
- element: ,
+ path: getRoute(PageID.LOGIN),
+ element: ,
},
{
- path: "testb",
- element: ,
+ path: getRoute(PageID.LOGOUT),
+ element: ,
},
+ ],
+});
+
+const DashboardRoutes = (): RouteObject => ({
+ element: ,
+ children: [
{
- path: "*",
- element: ,
+ path: getRoute(PageID.DASHBOARD_CONTRACT_LIST),
+ element: ,
+ },
+ {
+ path: getRoute(PageID.DASHBOARD_CONTRACT_DETAIL),
+ element: ,
},
+ ],
+});
+
+export function Routes() {
+ return useRoutes([
+ AuthRoutes(),
+ DashboardRoutes(),
{
- path: PATH.dashboard.root,
- element: ,
- children: [
- {
- path: getRoute(PageID.DASHBOARD_CONTRACT_LIST, PATH.dashboard.root),
- element: ,
- },
- {
- path: getRoute(PageID.DASHBOARD_CONTRACT_DETAIL, PATH.dashboard.root),
- element: ,
- },
- ],
+ path: "*",
+ element: ,
},
]);
}
const TestAPage = Loadable(lazy(() => import("pages/test/TestA")));
const TestBPage = Loadable(lazy(() => import("pages/test/TestB")));
+const Login = Loadable(lazy(() => import("pages/auth/login")));
+const Logout = Loadable(lazy(() => import("pages/auth/logout")));
const ContractList = Loadable(
lazy(() => import("pages/dashboard/contract/list"))
diff --git a/src/routes/path.ts b/src/routes/path.ts
index 99a98c6..bfe1a77 100644
--- a/src/routes/path.ts
+++ b/src/routes/path.ts
@@ -1,54 +1,75 @@
import { Dictionary } from "@types";
-import { PageID } from "codes/page";
-import { get, isNumber, isString, replace } from "lodash";
+import { PageID, TabID } from "codes/page";
+import { get, isArray, isNumber, isString, replace } from "lodash";
const DASHBOARD = "dashboard";
-const PREFIX = {
- list: "list",
- detail: "detail",
-};
-
export const PATH = {
+ login: "/login",
+ logout: "/logout",
dashboard: {
- root: "dashboard",
- contract: "contract",
+ root: "/dashboard",
+ contract: "/contract",
},
};
-const makePath = (paths: string[]): string => {
- return "/" + paths.join("/");
+// const makePath = (paths: string[]): string => {
+// return "/" + paths.join("/");
+// };
+
+// const makeListPageCallback = (path: string) => {
+// return (page: number) => {
+// return [path, String(page)].join("/");
+// };
+// };
+// const makeDashboardPath = (paths: string[]): string => {
+// return makePath([PATH.dashboard.root, ...paths]);
+// };
+
+type PathKey = [PageID, TabID?] | PageID;
+const makePathKey = (arg: PathKey): string => {
+ if (isArray(arg)) {
+ const tabStr = arg[1] !== undefined ? "/" + String(arg[1]) : "";
+ return String(arg[0]) + tabStr;
+ } else {
+ return String(arg);
+ }
};
-const makeListPageCallback = (path: string) => {
- return (page: number) => {
- return [path, "/", PREFIX.list, String(page)].join("/");
- };
-};
-const makeDashboardPath = (paths: string[]): string => {
- return makePath([PATH.dashboard.root, ...paths]);
+const getPageId = (key: PathKey): PageID => {
+ if (isArray(key)) {
+ return key[0];
+ } else {
+ return key;
+ }
};
-export const PATH_DASHBOARD = {
- contract: {
- list: makeListPageCallback(makeDashboardPath([PATH.dashboard.contract])),
- detail: makeDashboardPath([PATH.dashboard.contract, "detail"]),
- },
- sms: {
- list: makePath([DASHBOARD, "sms"]),
- },
+const getTabId = (key: PathKey): TabID => {
+ if (isArray(key)) {
+ return key[1] ?? TabID.NONE;
+ } else {
+ return TabID.NONE;
+ }
};
const PATHS = {
- [PageID.DASHBOARD_CONTRACT_LIST]: "/dashboard/contract/list/:page",
- [PageID.DASHBOARD_CONTRACT_DETAIL]: "/dashboard/contract/detail",
+ // 認証
+ [makePathKey(PageID.LOGIN)]: "/login",
+ [makePathKey(PageID.LOGOUT)]: "/logout",
+
+ [makePathKey(PageID.DASHBOARD_CONTRACT_LIST)]:
+ "/dashboard/contract/list/:page",
+ [makePathKey(PageID.DASHBOARD_CONTRACT_DETAIL)]: "/dashboard/contract/detail",
};
export type PathOption = {
page?: number;
query?: Dictionary;
};
-export function getPath(pageId: PageID, option?: PathOption) {
+export function getPath(key: PathKey, option?: PathOption) {
+ const pageId = getPageId(key);
+ const tabId = getTabId(key);
+
let path = getRoute(pageId);
// ページ番号解決
@@ -67,9 +88,13 @@ export function getPath(pageId: PageID, option?: PathOption) {
return path;
}
-export function getRoute(pageId: PageID, exclude?: string): string {
- let path = get(PATHS, pageId);
- if (!path) throw new Error("ルート未定義:" + pageId);
+export function getListPagePath(key: PathKey, page: number): string {
+ return getPath(key, { page });
+}
+
+export function getRoute(key: PathKey, exclude?: string): string {
+ let path = get(PATHS, makePathKey(key));
+ if (!path) throw new Error("ルート未定義:" + makePathKey(key));
if (exclude) {
path = replace(path, "/" + exclude + "/", "");
diff --git a/src/theme/index.tsx b/src/theme/index.tsx
index c722ade..b98edcb 100644
--- a/src/theme/index.tsx
+++ b/src/theme/index.tsx
@@ -1,5 +1,6 @@
import { ThemeProvider, createTheme } from "@mui/material";
import { HasChildren } from "@types";
+import { memo, useMemo } from "react";
let theme = createTheme({
palette: {
@@ -43,6 +44,13 @@ theme = {
},
},
},
+ MuiTableHead: {
+ styleOverrides: {
+ root: {
+ backgroundColor: "#D1E6D6",
+ },
+ },
+ },
MuiButton: {
styleOverrides: {
root: {
@@ -145,6 +153,9 @@ theme = {
};
type Props = HasChildren;
-export function AppThemeProvider({ children }: Props) {
- return {children};
+function AppThemeProvider({ children }: Props) {
+ const t = useMemo(() => theme, []);
+ return {children};
}
+
+export default memo(AppThemeProvider);
diff --git a/src/types/common.ts b/src/types/common.ts
new file mode 100644
index 0000000..78a57b0
--- /dev/null
+++ b/src/types/common.ts
@@ -0,0 +1,11 @@
+export type Data = {
+ id: string;
+ updated_id?: string;
+ updated_at?: string;
+ created_at?: string;
+ updated_by?: string;
+ created_by?: string;
+};
+export type HistoryData = {
+ history_id: string;
+} & Data;
diff --git a/src/types/contract.ts b/src/types/contract.ts
new file mode 100644
index 0000000..612d580
--- /dev/null
+++ b/src/types/contract.ts
@@ -0,0 +1,5 @@
+import { Data } from "./common";
+
+export type Contract = {
+ name: string;
+} & Data;
diff --git a/src/utils/axios.ts b/src/utils/axios.ts
new file mode 100644
index 0000000..9e70c5c
--- /dev/null
+++ b/src/utils/axios.ts
@@ -0,0 +1,20 @@
+import axios from "axios";
+// config
+import { HOST_API } from "config";
+
+// ----------------------------------------------------------------------
+
+const axiosInstance = axios.create({
+ baseURL: HOST_API,
+ withCredentials: true,
+});
+
+axiosInstance.interceptors.response.use(
+ (response) => response,
+ (error) =>
+ Promise.reject(
+ (error.response && error.response.data) || "Something went wrong"
+ )
+);
+
+export default axiosInstance;
diff --git a/src/utils/datetime.ts b/src/utils/datetime.ts
new file mode 100644
index 0000000..4475dfa
--- /dev/null
+++ b/src/utils/datetime.ts
@@ -0,0 +1,24 @@
+import { format, parseISO } from 'date-fns';
+
+type Input = Date | string | null | undefined;
+
+export const formatDateStr = (source: Input) => {
+ return formatToStr(source, 'yyyy/MM/dd');
+};
+
+export const formatDateTimeStr = (source: Date | string | null | undefined) => {
+ return formatToStr(source, 'yyyy/MM/dd HH:mm:ss');
+};
+
+const formatToStr = (source: Input, formatStr: string) => {
+ if (source === null || source === undefined) return '';
+ if (source instanceof Date) return format(source, formatStr);
+ return format(parseISO(source), formatStr);
+};
+
+export const now = () => {
+ return new Date();
+};
+export const nowStr = (): string => {
+ return formatDateTimeStr(now());
+};
diff --git a/tsconfig.json b/tsconfig.json
index 82c69f5..d7cc8ed 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,6 @@
{
"compilerOptions": {
- "target": "es5",
+ "target": "ES2022",
"lib": [
"dom",
"dom.iterable",
diff --git a/yarn.lock b/yarn.lock
index 78d3cb4..ee1b4ff 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1334,6 +1334,11 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.39.0.tgz#58b536bcc843f4cd1e02a7e6171da5c040f4d44b"
integrity sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==
+"@hookform/resolvers@^3.1.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-3.1.0.tgz#ff83ef4aa6078173201da131ceea4c3583b67034"
+ integrity sha512-z0A8K+Nxq+f83Whm/ajlwE6VtQlp/yPHZnXw7XWVPIGm1Vx0QV8KThU3BpbBRfAZ7/dYqCKKBNnQh85BkmBKkA==
+
"@humanwhocodes/config-array@^0.11.8":
version "0.11.8"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
@@ -1669,6 +1674,20 @@
prop-types "^15.8.1"
react-is "^18.2.0"
+"@mui/base@5.0.0-alpha.128":
+ version "5.0.0-alpha.128"
+ resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.128.tgz#8ce4beb971ac989df0b1d3b2bd3e9274dbfa604f"
+ integrity sha512-wub3wxNN+hUp8hzilMlXX3sZrPo75vsy1cXEQpqdTfIFlE9HprP1jlulFiPg5tfPst2OKmygXr2hhmgvAKRrzQ==
+ dependencies:
+ "@babel/runtime" "^7.21.0"
+ "@emotion/is-prop-valid" "^1.2.0"
+ "@mui/types" "^7.2.4"
+ "@mui/utils" "^5.12.3"
+ "@popperjs/core" "^2.11.7"
+ clsx "^1.2.1"
+ prop-types "^15.8.1"
+ react-is "^18.2.0"
+
"@mui/core-downloads-tracker@^5.12.2":
version "5.12.2"
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.12.2.tgz#4a0186d25b01d693171366e1c00de0e7c8c35f6a"
@@ -1681,6 +1700,20 @@
dependencies:
"@babel/runtime" "^7.21.0"
+"@mui/lab@^5.0.0-alpha.129":
+ version "5.0.0-alpha.129"
+ resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.129.tgz#e940aeef995175586e058cad36e801502730b670"
+ integrity sha512-niv2mFgSTgdrRJXbWoX9pIivhe80BaFXfdWajXe1bS8VYH3Y5WyJpk8KiU3rbHyJswbFEGd8N6EBBrq11X8yMA==
+ dependencies:
+ "@babel/runtime" "^7.21.0"
+ "@mui/base" "5.0.0-alpha.128"
+ "@mui/system" "^5.12.3"
+ "@mui/types" "^7.2.4"
+ "@mui/utils" "^5.12.3"
+ clsx "^1.2.1"
+ prop-types "^15.8.1"
+ react-is "^18.2.0"
+
"@mui/material@^5.12.2":
version "5.12.2"
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.12.2.tgz#c3fcc94e523d9e673e2e045dfad04d12ab454a80"
@@ -1708,6 +1741,15 @@
"@mui/utils" "^5.12.0"
prop-types "^15.8.1"
+"@mui/private-theming@^5.12.3":
+ version "5.12.3"
+ resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.12.3.tgz#f5e4704e25d9d91b906561cae573cda8f3801e10"
+ integrity sha512-o1e7Z1Bp27n4x2iUHhegV4/Jp6H3T6iBKHJdLivS5GbwsuAE/5l4SnZ+7+K+e5u9TuhwcAKZLkjvqzkDe8zqfA==
+ dependencies:
+ "@babel/runtime" "^7.21.0"
+ "@mui/utils" "^5.12.3"
+ prop-types "^15.8.1"
+
"@mui/styled-engine@^5.12.0":
version "5.12.0"
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.12.0.tgz#44640cad961adcc9413ae32116237cd1c8f7ddb0"
@@ -1718,6 +1760,16 @@
csstype "^3.1.2"
prop-types "^15.8.1"
+"@mui/styled-engine@^5.12.3":
+ version "5.12.3"
+ resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.12.3.tgz#3307643d52c81947a624cdd0437536cc8109c4f0"
+ integrity sha512-AhZtiRyT8Bjr7fufxE/mLS+QJ3LxwX1kghIcM2B2dvJzSSg9rnIuXDXM959QfUVIM3C8U4x3mgVoPFMQJvc4/g==
+ dependencies:
+ "@babel/runtime" "^7.21.0"
+ "@emotion/cache" "^11.10.8"
+ csstype "^3.1.2"
+ prop-types "^15.8.1"
+
"@mui/system@^5.12.1":
version "5.12.1"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.12.1.tgz#8452bc03159f0a6725b96bde1dee1316e308231b"
@@ -1732,6 +1784,20 @@
csstype "^3.1.2"
prop-types "^15.8.1"
+"@mui/system@^5.12.3":
+ version "5.12.3"
+ resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.12.3.tgz#306b3cdffa3046067640219c1e5dd7e3dae38ff9"
+ integrity sha512-JB/6sypHqeJCqwldWeQ1MKkijH829EcZAKKizxbU2MJdxGG5KSwZvTBa5D9qiJUA1hJFYYupjiuy9ZdJt6rV6w==
+ dependencies:
+ "@babel/runtime" "^7.21.0"
+ "@mui/private-theming" "^5.12.3"
+ "@mui/styled-engine" "^5.12.3"
+ "@mui/types" "^7.2.4"
+ "@mui/utils" "^5.12.3"
+ clsx "^1.2.1"
+ csstype "^3.1.2"
+ prop-types "^15.8.1"
+
"@mui/types@^7.2.4":
version "7.2.4"
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.4.tgz#b6fade19323b754c5c6de679a38f068fd50b9328"
@@ -1748,6 +1814,17 @@
prop-types "^15.8.1"
react-is "^18.2.0"
+"@mui/utils@^5.12.3":
+ version "5.12.3"
+ resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.12.3.tgz#3fa3570dac7ec66bb9cc84ab7c16ab6e1b7200f2"
+ integrity sha512-D/Z4Ub3MRl7HiUccid7sQYclTr24TqUAQFFlxHQF8FR177BrCTQ0JJZom7EqYjZCdXhwnSkOj2ph685MSKNtIA==
+ dependencies:
+ "@babel/runtime" "^7.21.0"
+ "@types/prop-types" "^15.7.5"
+ "@types/react-is" "^16.7.1 || ^17.0.0"
+ prop-types "^15.8.1"
+ react-is "^18.2.0"
+
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
version "5.1.1-v1"
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"
@@ -2040,6 +2117,13 @@
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc"
integrity sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==
+"@types/axios@^0.14.0":
+ version "0.14.0"
+ resolved "https://registry.yarnpkg.com/@types/axios/-/axios-0.14.0.tgz#ec2300fbe7d7dddd7eb9d3abf87999964cafce46"
+ integrity sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==
+ dependencies:
+ axios "*"
+
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14":
version "7.20.0"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891"
@@ -2103,6 +2187,13 @@
dependencies:
"@types/node" "*"
+"@types/date-fns@^2.6.0":
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/@types/date-fns/-/date-fns-2.6.0.tgz#b062ca46562002909be0c63a6467ed173136acc1"
+ integrity sha512-9DSw2ZRzV0Tmpa6PHHJbMcZn79HHus+BBBohcOaDzkK/G3zMjDUDYjJIWBFLbkh+1+/IOS0A59BpQfdr37hASg==
+ dependencies:
+ date-fns "*"
+
"@types/eslint-scope@^3.7.3":
version "3.7.4"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
@@ -2952,6 +3043,15 @@ axe-core@^4.6.2:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf"
integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==
+axios@*, axios@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
+ integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
+ dependencies:
+ follow-redirects "^1.15.0"
+ form-data "^4.0.0"
+ proxy-from-env "^1.1.0"
+
axobject-query@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1"
@@ -3369,7 +3469,7 @@ cliui@^7.0.2:
strip-ansi "^6.0.0"
wrap-ansi "^7.0.0"
-clsx@^1.2.1:
+clsx@^1.1.0, clsx@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
@@ -3798,6 +3898,13 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
+date-fns@*, date-fns@^2.30.0:
+ version "2.30.0"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
+ integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
+ dependencies:
+ "@babel/runtime" "^7.21.0"
+
debug@2.6.9, debug@^2.6.0:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -4771,7 +4878,7 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
-follow-redirects@^1.0.0:
+follow-redirects@^1.0.0, follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
@@ -4811,6 +4918,15 @@ form-data@^3.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
+form-data@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+ integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.8"
+ mime-types "^2.1.12"
+
forwarded@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
@@ -5012,6 +5128,11 @@ globby@^11.0.4, globby@^11.1.0:
merge2 "^1.4.1"
slash "^3.0.0"
+goober@^2.0.33:
+ version "2.1.13"
+ resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.13.tgz#e3c06d5578486212a76c9eba860cbc3232ff6d7c"
+ integrity sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==
+
gopd@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
@@ -6715,6 +6836,14 @@ normalize-url@^6.0.1:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
+notistack@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/notistack/-/notistack-3.0.1.tgz#daf59888ab7e2c30a1fa8f71f9cba2978773236e"
+ integrity sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==
+ dependencies:
+ clsx "^1.1.0"
+ goober "^2.0.33"
+
npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
@@ -7691,6 +7820,11 @@ prop-types@^15.6.2, prop-types@^15.8.1:
object-assign "^4.1.1"
react-is "^16.13.1"
+property-expr@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4"
+ integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==
+
proxy-addr@~2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
@@ -7699,6 +7833,11 @@ proxy-addr@~2.0.7:
forwarded "0.2.0"
ipaddr.js "1.9.1"
+proxy-from-env@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+ integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
psl@^1.1.33:
version "1.9.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
@@ -7815,6 +7954,11 @@ react-error-overlay@^6.0.11:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
+react-hook-form@^7.43.9:
+ version "7.43.9"
+ resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.43.9.tgz#84b56ac2f38f8e946c6032ccb760e13a1037c66d"
+ integrity sha512-AUDN3Pz2NSeoxQ7Hs6OhQhDr6gtF9YRuutGDwPQqhSUAHJSgGl2VeY3qN19MG0SucpjgDiuMJ4iC5T5uB+eaNQ==
+
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -8885,6 +9029,11 @@ thunky@^1.0.2:
resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d"
integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==
+tiny-case@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03"
+ integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==
+
tmpl@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
@@ -8907,6 +9056,11 @@ toidentifier@1.0.1:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
+toposort@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
+ integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==
+
tough-cookie@^4.0.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874"
@@ -9002,6 +9156,11 @@ type-fest@^0.21.3:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
+type-fest@^2.19.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
+ integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
+
type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@@ -9686,3 +9845,13 @@ yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+
+yup@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/yup/-/yup-1.1.1.tgz#49dbcf5ae7693ed0a36ed08a9e9de0a09ac18e6b"
+ integrity sha512-KfCGHdAErqFZWA5tZf7upSUnGKuTOnsI3hUsLr7fgVtx+DK04NPV01A68/FslI4t3s/ZWpvXJmgXhd7q6ICnag==
+ dependencies:
+ property-expr "^2.0.5"
+ tiny-case "^1.0.3"
+ toposort "^2.0.2"
+ type-fest "^2.19.0"