Merged PR 51: タスク 1468: 部品component作成(ヘッダー・フッター)

## 概要
[Task: 1468](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_sprints/taskboard/OMDSDictation%20%E3%83%81%E3%83%BC%E3%83%A0/OMDSDictation/%E3%82%B9%E3%83%97%E3%83%AA%E3%83%B3%E3%83%88%204-2?workitem=1468)

- ヘッダーcomponentを作成
  - ログイン前とログイン後でヘッダーが異なるので各ページに配置するようにした
  - 呼び出すcomponentは一つとして作成し、内部でヘッダーを切り替えるようにした
- フッターcomponentを作成
  - ログイン前とログイン後でページのデザインが異なるのでヘッダー同様、各ページに配置することにした

## レビューポイント
- ヘッダーの作成方法に問題はないか

## UIの変更
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task1468?csf=1&web=1&e=K2tFjK

## 動作確認状況
- 型チェック
- ローカルで動作確認

## 補足
This commit is contained in:
saito.k 2023-03-27 12:05:56 +00:00
parent dfd9abc1c3
commit b4cd0208e6
26 changed files with 448 additions and 85 deletions

View File

@ -13,6 +13,7 @@ import "./styles/GlobalStyle.css";
const App = (): JSX.Element => {
const dispatch = useDispatch();
const { instance } = useMsal();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [t, i18n] = useTranslation();
const pca = new PublicClientApplication(msalConfig);
useEffect(() => {

View File

@ -11,10 +11,31 @@ const AppRouter: React.FC = () => (
<Routes>
<Route path="/" element={<TopPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/AuthError" element={<AuthErrorPage />} />
<Route path="/authError" element={<AuthErrorPage />} />
<Route path="/signup" element={<SignupPage completeTo="/" />} />
<Route
path="/XXX"
path="/xxx"
element={<RouteAuthGuard component={<SamplePage />} />}
/>
{/* XXX ヘッダーの挙動確認のため仮のページを作成 */}
<Route
path="/account"
element={<RouteAuthGuard component={<SamplePage />} />}
/>
<Route
path="/user"
element={<RouteAuthGuard component={<SamplePage />} />}
/>
<Route
path="/dictations"
element={<RouteAuthGuard component={<SamplePage />} />}
/>
<Route
path="/license"
element={<RouteAuthGuard component={<SamplePage />} />}
/>
<Route
path="/workflow"
element={<RouteAuthGuard component={<SamplePage />} />}
/>
<Route path="*" element={<NotFoundPage />} />

View File

@ -0,0 +1,10 @@
.footer {
grid-area: footer;
padding: 0.6rem 0;
text-align: center;
font-size: 11px;
line-height: 1.45;
letter-spacing: 0.4px;
font-weight: normal;
color: #999999;
}

View File

@ -0,0 +1,4 @@
declare const classNames: {
readonly footer: "footer";
};
export = classNames;

View File

@ -0,0 +1,10 @@
import React from "react";
import styles from "./footer.module.scss";
const Footer: React.FC = () => (
<footer className={`${styles.footer}`}>
<div>&copy; OM Digital Solutions 2023</div>
</footer>
);
export default Footer;

View File

@ -0,0 +1,12 @@
import { HeaderMenus, LoginedPaths } from "./types";
export const HEADER_MENUS: { label: HeaderMenus; path: LoginedPaths }[] = [
{ label: "Account", path: "/account" },
{ label: "Dictations", path: "/dictations" },
{ label: "License", path: "/license" },
{ label: "User", path: "/user" },
{ label: "Workflow", path: "/workflow" },
{ label: "XXX", path: "/xxx" }, // XXX 仮のタブ
];
export const HEADER_NAME = "ODMS Cloud";

View File

@ -0,0 +1,47 @@
.header {
grid-area: header;
position: relative;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
box-shadow: 0 0 3px #282828;
background: linear-gradient(#ffffff, #ffffff 70%, #f0f0f0 100%);
z-index: 4;
}
.headerLogo {
margin: 1.8rem 2rem 1rem;
font-size: 1.71rem;
line-height: 2rem;
letter-spacing: 0.07rem;
font-weight: normal;
}
.headerLogo img {
width: 198px;
}
.headerSub {
margin: 1.8rem 2rem 1rem;
font-size: 1.2rem;
line-height: 2rem;
letter-spacing: 0.07rem;
font-weight: normal;
font-weight: 600;
text-align: right;
}
@media only screen and (min-width: 1280px) {
.header.home {
display: block;
padding-top: 30vh;
padding-left: 40%;
background: none;
box-shadow: none;
}
.header.home .headerSub {
font-size: 1.4rem;
line-height: 2rem;
letter-spacing: 0.07rem;
font-weight: 600;
text-align: left;
}
}

View File

@ -0,0 +1,7 @@
declare const classNames: {
readonly header: "header";
readonly headerLogo: "headerLogo";
readonly headerSub: "headerSub";
readonly home: "home";
};
export = classNames;

View File

@ -0,0 +1,25 @@
import React from "react";
import { useLocation } from "react-router-dom";
import LoginedHeader from "./loginedHeader";
import NotLoginHeader from "./notLoginHeader";
import { isLoginPaths } from "./utils";
interface HeaderProps {
userName?: string;
// userRole: string; ログインユーザーのロールに応じてタブの活性非活性に使用する想定
}
// ヘッダー切り替え用のcomponent
const Header: React.FC<HeaderProps> = (props) => {
const { userName } = props;
const location = useLocation();
return getHeader(location.pathname, userName);
};
export default Header;
const getHeader = (path: string, userName?: string) => {
if (isLoginPaths(path) && userName) {
return <LoginedHeader name={userName} activePath={path} />;
}
return <NotLoginHeader isMobile={path === "/"} />;
};

View File

@ -0,0 +1,87 @@
.header {
grid-area: header;
position: relative;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
box-shadow: 0 0 3px #282828;
background: linear-gradient(#ffffff, #ffffff 70%, #f0f0f0 100%);
z-index: 4;
}
.headerLogo {
margin: 1.2rem 2rem 1rem;
font-size: 1.71rem;
line-height: 2rem;
letter-spacing: 0.07rem;
font-weight: normal;
}
.headerLogo img {
width: 198px;
}
.headerSub {
margin: 1.4rem 2rem 1rem;
font-size: 1.2rem;
line-height: 2rem;
letter-spacing: 0.07rem;
font-weight: normal;
font-weight: 600;
text-align: right;
}
.headerMenu {
width: 100%;
}
.headerMenu ul {
display: flex;
flex-wrap: wrap;
padding: 0 2rem;
}
.headerMenu ul li {
font-size: 1rem;
line-height: 2rem;
letter-spacing: 0.07rem;
font-weight: normal;
border-left: 1px #e6e6e6 solid;
}
.headerMenu ul li a {
display: block;
padding: 0 2rem;
color: #333333;
text-decoration: none;
-moz-transition: all 0.3s ease-out;
-ms-transition: all 0.3s ease-out;
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.headerMenu ul li a:hover {
background: #fafafa;
text-decoration: underline;
}
.headerMenu ul li a.isActive {
font-weight: 600;
pointer-events: none;
position: relative;
}
.headerMenu ul li a.isActive::after {
content: "";
border-right: 0.6rem transparent solid;
border-bottom: 0.6rem #282828 solid;
border-left: 0.6rem transparent solid;
position: absolute;
bottom: -4px;
left: 50%;
transform: translateX(-50%);
}
.accountInfo {
position: absolute;
right: 2rem;
bottom: 0.3rem;
font-size: 0.8rem;
line-height: 1.6rem;
letter-spacing: 0.04rem;
font-weight: normal;
}
.accountIcon {
width: 1.4rem;
vertical-align: top;
}

View File

@ -0,0 +1,10 @@
declare const classNames: {
readonly header: "header";
readonly headerLogo: "headerLogo";
readonly headerSub: "headerSub";
readonly headerMenu: "headerMenu";
readonly isActive: "isActive";
readonly accountInfo: "accountInfo";
readonly accountIcon: "accountIcon";
};
export = classNames;

View File

@ -0,0 +1,51 @@
import React from "react";
import styles from "./loginedHeader.module.scss";
import logo from "../../assets/images/OMS_logo_black.svg";
import ac from "../../assets/images/account_circle.svg";
import { LoginedPaths } from "./types";
import { HEADER_MENUS, HEADER_NAME } from "./constants";
interface HeaderProps {
// userRole: "user" | "partner"; ログインユーザーのロールに応じてタブの活性非活性に使用する想定
name: string;
activePath: LoginedPaths;
}
// ログイン後のヘッダー
const LoginedHeader: React.FC<HeaderProps> = (props: HeaderProps) => {
const { name, activePath } = props;
return (
<header className={styles.header}>
<div className={styles.headerLogo}>
<img src={logo} alt="OM System" />
</div>
<div className={styles.headerSub}>{HEADER_NAME}</div>
<div className={styles.headerMenu}>
<ul>
{HEADER_MENUS.map((x) => (
<li key={x.label}>
<a
href={x.path}
className={
activePath.toUpperCase() === x.path.toUpperCase()
? styles.isActive
: ""
}
>
{x.label}
</a>
</li>
))}
</ul>
<p className={styles.accountInfo}>
<img src={ac} alt="" className={styles.accountIcon} />
<span>{name}</span>
</p>
</div>
</header>
);
};
export default LoginedHeader;

View File

@ -0,0 +1,27 @@
import React from "react";
import styles from "./header.module.scss";
import logo from "../../assets/images/OMS_logo_black.svg";
import { HEADER_NAME } from "./constants";
interface NotLoginHeaderProps {
isMobile?: boolean;
}
// ログインしていない時のヘッダー
const NotLoginHeader: React.FC<NotLoginHeaderProps> = (
props: NotLoginHeaderProps
) => {
const { isMobile } = props;
return (
<header className={`${styles.header} ${isMobile && styles.home}`}>
<div className={`${styles.headerLogo}`}>
<img src={logo} alt="OM System" />
</div>
<p className={`${styles.headerSub}`}>{HEADER_NAME}</p>
</header>
);
};
NotLoginHeader.defaultProps = {
isMobile: false,
};
export default NotLoginHeader;

View File

@ -0,0 +1,20 @@
// ページパス
export type Paths = LoginedPaths | "/" | "/signup" | "login";
// ログイン後のヘッダータブ
export type HeaderMenus =
| "Account"
| "User"
| "License"
| "Dictations"
| "Workflow"
| "XXX";
// ログイン後に遷移しうるパス
export type LoginedPaths =
| "/account"
| "/user"
| "/license"
| "/dictations"
| "/workflow"
| "/xxx";

View File

@ -0,0 +1,21 @@
import { LoginedPaths } from "./types";
// ログイン後のパスかどうか判定
export const isLoginPaths = (d: string): d is LoginedPaths => {
// caseに入力補完で取りうるリテラルしか出なくする
const type = d as LoginedPaths;
switch (type) {
case "/account":
case "/user":
case "/license":
case "/dictations":
case "/workflow":
case "/xxx":
return true;
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _: never = type;
return false;
}
}
};

View File

@ -1,9 +1,7 @@
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import React from "react";
export const AuthErrorPage = (): JSX.Element => (
<div>
<UpdateTokenTimer />
<p></p>
<br />
</div>

View File

@ -1,6 +1,8 @@
import { InteractionStatus, SilentRequest } from "@azure/msal-browser";
import { useIsAuthenticated, useMsal } from "@azure/msal-react";
import { AppDispatch } from "app/store";
import Footer from "components/footer";
import Header from "components/header";
import { loginAsync } from "features/login";
import React, { useCallback, useEffect } from "react";
import { useDispatch } from "react-redux";
@ -26,7 +28,7 @@ const LoginPage: React.FC = (): JSX.Element => {
});
}
if (meta.requestStatus === "fulfilled") {
navigate("/XXX");
navigate("/xxx");
}
}, [accounts, dispatch, instance, navigate]);
@ -45,7 +47,13 @@ const LoginPage: React.FC = (): JSX.Element => {
navigate,
]);
return <h3>loading ...</h3>;
return (
<>
<Header />
<h3>loading ...</h3>
<Footer />
</>
);
};
export default LoginPage;

View File

@ -1,26 +1,33 @@
import { useMsal } from "@azure/msal-react";
import { AppDispatch } from "app/store";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import Footer from "components/footer";
import Header from "components/header";
import { clearToken } from "features/auth";
import React from "react";
import { useDispatch } from "react-redux";
import styles from "./sample.module.scss";
const SamplePage: React.FC = (): JSX.Element => {
const { instance } = useMsal();
const dispatch: AppDispatch = useDispatch();
return (
<div>
<div className={styles.wrap}>
<Header userName="XXXXXX" />
<UpdateTokenTimer />
<h1>hello world!!</h1>
<button
type="button"
onClick={() => {
instance.logout({ postLogoutRedirectUri: "/" });
dispatch(clearToken());
}}
>
sign out
</button>
<div>
<button
type="button"
className={styles.button}
onClick={() => {
instance.logout({ postLogoutRedirectUri: "/" });
dispatch(clearToken());
}}
>
sign out
</button>
</div>
<Footer />
</div>
);
};

View File

@ -0,0 +1,21 @@
.wrap {
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-columns: 1fr;
grid-template-areas: "header" "main" "footer";
min-height: 100vh;
}
.button {
display: block;
padding: 12px 0;
color: #ffffff;
text-decoration: none;
background: #005eb8;
border: 1px #005eb8 solid;
margin-left: 100px;
cursor: pointer;
:hover {
background: rgba(0, 94, 184, 0.7);
}
}

View File

@ -0,0 +1,5 @@
declare const classNames: {
readonly wrap: "wrap";
readonly button: "button";
};
export = classNames;

View File

@ -1,3 +1,5 @@
import Footer from "components/footer";
import Header from "components/header";
import { selectPageState } from "features/signup/selectors";
import React from "react";
import { useSelector } from "react-redux";
@ -9,7 +11,13 @@ const SignupPage: React.FC<{ completeTo: string }> = ({
}): JSX.Element => {
const state = useSelector(selectPageState);
return getComponent(state, completeTo);
return (
<>
<Header />
{getComponent(state, completeTo)}
<Footer />
</>
);
};
// 現在のサインアップ画面の状態に応じて表示Componentを出し分ける

View File

@ -6,39 +6,6 @@
min-height: 100vh;
}
.header {
grid-area: header;
position: relative;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
box-shadow: 0 0 3px #282828;
background: linear-gradient(#ffffff, #ffffff 70%, #f0f0f0 100%);
z-index: 4;
}
.headerLogo {
margin: 1.8rem 2rem 1rem;
font-size: 1.71rem;
line-height: 2rem;
letter-spacing: 0.07rem;
font-weight: normal;
}
.headerLogo img {
width: 198px;
}
.headerSub {
margin: 1.8rem 2rem 1rem;
font-size: 1.2rem;
line-height: 2rem;
letter-spacing: 0.07rem;
font-weight: normal;
font-weight: 600;
text-align: right;
}
.main {
grid-area: main;
}
@ -171,24 +138,6 @@
}
}
@media only screen and (min-width: 1280px) {
.header.home {
display: block;
padding-top: 30vh;
padding-left: 40%;
background: none;
box-shadow: none;
}
.header.home .headerSub {
font-size: 1.4rem;
line-height: 2rem;
letter-spacing: 0.07rem;
font-weight: 600;
text-align: left;
}
}
.pgHome > div {
width: 400px;
margin: 15vh auto 0;

View File

@ -1,8 +1,5 @@
declare const classNames: {
readonly wrap: "wrap";
readonly header: "header";
readonly headerLogo: "headerLogo";
readonly headerSub: "headerSub";
readonly main: "main";
readonly mainSmall: "mainSmall";
readonly footer: "footer";

View File

@ -3,8 +3,9 @@ import { loginRequest } from "common/msalConfig";
import React from "react";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import Header from "components/header";
import Footer from "components/footer";
import { LANGUAGE_LIST } from "../../features/top/constants";
import logo from "../../assets/images/OMS_logo_black.svg";
import arrow_forward from "../../assets/images/arrow_forward.svg";
import arrow_forward_bule from "../../assets/images/arrow_forward_blue.svg";
import styles from "./TopPage.module.scss";
@ -16,14 +17,7 @@ const TopPage: React.FC = (): JSX.Element => {
return (
<div className={`${styles.wrap} ${styles.home}`}>
<header className={`${styles.header} ${styles.home}`}>
<div className={`${styles.headerLogo}`}>
<img src={logo} alt="OM System" />
</div>
<p className={`${styles.headerSub}`}>
{t(getTranslationID("common.label.headerTitle"))}
</p>
</header>
<Header />
<main className={`${styles.main} ${styles.home}`}>
<section className={`${styles.pgHome}`}>
@ -87,10 +81,7 @@ const TopPage: React.FC = (): JSX.Element => {
</div>
</section>
</main>
<footer className={`${styles.footer}`}>
<div>&copy; {t(getTranslationID("common.label.copyRight"))}</div>
</footer>
<Footer />
</div>
);
};

View File

@ -0,0 +1,21 @@
.wrap {
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-columns: 1fr;
grid-template-areas: "header" "main" "footer";
min-height: 100vh;
}
.wrap.home {
background: url("../assets/images/top-bg04.png") no-repeat bottom center;
background-size: cover;
}
@media only screen and (min-width: 1280px) {
.wrap.home {
display: grid;
grid-template-rows: 1fr auto;
grid-template-columns: 45% 1fr;
grid-template-areas: "header main" "footer footer";
min-height: 100vh;
}
}

View File

@ -0,0 +1,5 @@
declare const classNames: {
readonly wrap: "wrap";
readonly home: "home";
};
export = classNames;