Repository: shinonome-inc/kota-frontend-basic-2025-todoapp
Pull request #12: Alertの実装
Author: login: harada-0
Created: 2025-04-03T12:58:09Z | Merged: 2025-04-07T01:49:17Z

Alertの実装とmainページの作成


Diff:
diff --git a/src/components/Atoms/Alert/index.jsx b/src/components/Atoms/Alert/index.jsx
new file mode 100644
index 0000000..bcedf7b
--- /dev/null
+++ b/src/components/Atoms/Alert/index.jsx
@@ -0,0 +1,40 @@
+import React from "react";
+import styled from "styled-components";
+import COLOR from "../../../variables/color";
+import FONTFAMILY from "../../../variables/font_family";
+import TEXT from "../../../variables/texts";
+import BREAKPOINT from "../../../variables/breakpoint";
+
+export default function Alert({ visible, errorText }) {
+  return (
+    <StyledAlertWrapper visible={visible}>
+      <StyledAlert>{errorText}</StyledAlert>
+    </StyledAlertWrapper>
+  );
+}
+
+const StyledAlertWrapper = styled.div`
+  position: absolute;
+  left: 50%;
+  top: 80px;
+  background-color: ${COLOR.RED};
+  border-radius: 4px;
+  width: 400px;
+  transition: opacity 0.5s ease-in-out, transform 0.5s ease-in-out;
+  transform: ${(props) =>
+    props.visible ? "translate(-50%, 40%)" : "translate(-50%, 0)"};
+  opacity: ${(props) => (props.visible ? 1 : 0)};
+  @media (max-width: ${BREAKPOINT.MEDIUM}) {
+    top: 40px;
+    max-width: 280px;
+    width: 100%;
+  }
+`;
+
+const StyledAlert = styled.div`
+  padding: 10px 20px;
+  color: ${COLOR.WHITE};
+  ${TEXT.S};
+  font-family: ${FONTFAMILY.NOTO_SANS};
+  text-align: center;
+`;
diff --git a/src/components/Atoms/Alert/index.stories.jsx b/src/components/Atoms/Alert/index.stories.jsx
new file mode 100644
index 0000000..242e95a
--- /dev/null
+++ b/src/components/Atoms/Alert/index.stories.jsx
@@ -0,0 +1,10 @@
+import Alert from "./index";
+
+export default { component: Alert };
+
+export const Default = {
+  args: {
+    visible: true,
+    errorText: "タスクの名前が設定されていません。",
+  },
+};
diff --git a/src/components/Organisms/AlertManager/index.jsx b/src/components/Organisms/AlertManager/index.jsx
new file mode 100644
index 0000000..2ba9aec
--- /dev/null
+++ b/src/components/Organisms/AlertManager/index.jsx
@@ -0,0 +1,22 @@
+import React, { useEffect } from "react";
+import { useAlertHandlerContext } from "../../../contexts/alert_handler";
+import Alert from "../../Atoms/Alert";
+
+export const AlertManager = () => {
+  const AlertHandlerContext = useAlertHandlerContext();
+
+  useEffect(() => {
+    if (AlertHandlerContext.visible) {
+      setTimeout(() => {
+        AlertHandlerContext.closeAlert();
+      }, 5000);
+    }
+  }, [AlertHandlerContext]);
+
+  return (
+    <Alert
+      visible={AlertHandlerContext.visible}
+      errorText={AlertHandlerContext.errorText}
+    />
+  );
+};
diff --git a/src/components/Organisms/TodoCard/index.jsx b/src/components/Organisms/TodoCard/index.jsx
index 0b64471..daceef9 100644
--- a/src/components/Organisms/TodoCard/index.jsx
+++ b/src/components/Organisms/TodoCard/index.jsx
@@ -4,8 +4,11 @@ import { AddTaskButton } from "../../Atoms/AddTaskButton/index";
 import Task from "../../Molecules/index";
 import COLOR from "../../../variables/color";
 import BREAKPOINT from "../../../variables/breakpoint";
+import { useAlertHandlerContext } from "../../../contexts/alert_handler";
 
 export default function TodoCard() {
+  const AlertHandlerContext = useAlertHandlerContext();
+
   const [taskList, setTaskList] = useState(() => {
     const savedTask = localStorage.getItem("localKey");
     return savedTask ? JSON.parse(savedTask) : [];
@@ -28,7 +31,8 @@ export default function TodoCard() {
   };
 
   const onTaskNameChange = (value, index) => {
-    if (value.trim() === "") {
+    if (value === "") {
+      AlertHandlerContext.setAlert("タスクの名前が設定されていません。");
       return setTaskList(taskList.filter((_, i) => i !== index));
     } else {
       return setTaskList(
diff --git a/src/components/Pages/MainPage/index.stories.jsx b/src/components/Pages/MainPage/index.stories.jsx
index 92e015d..19320a2 100644
--- a/src/components/Pages/MainPage/index.stories.jsx
+++ b/src/components/Pages/MainPage/index.stories.jsx
@@ -1,5 +1,20 @@
+import React from "react";
 import MainPage from "./index";
+import { AlertHandlerProvider } from "../../../contexts/alert_handler";
+import { AlertManager } from "../../Organisms/AlertManager";
 
 export default { component: MainPage };
+const Template = (args) => <MainPage {...args} />;
+export const Default = Template.bind({});
+Default.args = {};
 
-export const Default = {};
+Default.decorators = [
+  (Story) => (
+    <div>
+      <AlertHandlerProvider>
+        <AlertManager />
+        <Story />
+      </AlertHandlerProvider>
+    </div>
+  ),
+];
diff --git a/src/contexts/alert_handler.jsx b/src/contexts/alert_handler.jsx
new file mode 100644
index 0000000..65588cb
--- /dev/null
+++ b/src/contexts/alert_handler.jsx
@@ -0,0 +1,32 @@
+import React, { createContext, useContext, useState } from "react";
+
+const AlertHandlerContext = createContext();
+
+export const AlertHandlerProvider = ({ children }) => {
+  const [alertState, setAlertState] = useState({
+    visible: false,
+    errorText: "",
+  });
+
+  const setAlert = (errorText) => {
+    setAlertState({ visible: true, errorText: errorText });
+  };
+
+  const closeAlert = () => {
+    setAlertState({ visible: false, errorText: "" });
+  };
+
+  const contextValue = {
+    ...alertState,
+    setAlert: setAlert,
+    closeAlert: closeAlert,
+  };
+
+  return (
+    <AlertHandlerContext.Provider value={contextValue}>
+      {children}
+    </AlertHandlerContext.Provider>
+  );
+};
+
+export const useAlertHandlerContext = () => useContext(AlertHandlerContext);
diff --git a/src/index.jsx b/src/index.jsx
index 149f1e2..c79f78a 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -1,6 +1,14 @@
 import React from "react";
 import ReactDOM from "react-dom";
 import MainPage from "./components/Pages/MainPage";
+import { AlertHandlerProvider } from "./contexts/alert_handler";
 import "./style.css";
+import { AlertManager } from "./components/Organisms/AlertManager";
 
-ReactDOM.render(<MainPage />, document.getElementById("app"));
+ReactDOM.render(
+  <AlertHandlerProvider>
+    <MainPage />
+    <AlertManager />
+  </AlertHandlerProvider>,
+  document.getElementById("app")
+);


Reviews:
user: login: luck-tech
body: 追加でUIとコードレビュー
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-03T18:19:41Z

Review comments:
diff_hunk: @@ -0,0 +1,9 @@
+import Alert from "./index";
+
+export default { component: Alert };
+
+export const Default = {
+  args: {
+    isActive: true,
+  },
path: src/components/Atoms/Alert/index.stories.jsx
user: login: luck-tech
body: これだけだと、StorybookでAlertが見えないのでは？
created_at: 2025-04-03T18:16:52Z
author_association: NONE
diff_hunk: @@ -0,0 +1,32 @@
+import React from "react";
+import styled from "styled-components";
+import COLOR from "../../../variables/color";
+import FONTFAMILY from "../../../variables/font_family";
+import TEXT from "../../../variables/texts";
+import BREAKPOINT from "../../../variables/breakpoint";
+
+export default function Alert({ visible, errorText }) {
+  return <StyledAlert visible={visible}>{errorText}</StyledAlert>;
+}
+
+const StyledAlert = styled.div`
+  position: absolute;
+  left: 50%;
+  top: 80px;
+  color: ${COLOR.WHITE};
+  background-color: ${COLOR.RED};
+  padding: 10px 20px;
+  ${TEXT.S};
+  font-family: ${FONTFAMILY.NOTO_SANS};
+  border-radius: 4px;
+  max-width: 400px;
+  text-align: center;
+  transition: opacity 0.5s ease-in-out, transform 0.5s ease-in-out;
+  transform: ${(props) =>
+    props.visible ? "translate(-50%, -40%)" : "translate(0, 0)"};
path: src/components/Atoms/Alert/index.jsx
user: login: luck-tech
body: storybookでAlertが表示されるようになったら、このアニメーションが変なことがわかると思うので、これも修正してみて！
created_at: 2025-04-03T18:19:11Z
author_association: NONE

PR comments:
user: login: luck-tech
created_at: 2025-04-03T18:15:56Z
body: ## Alertを表示する上で問題は2つ
段階的に解消していこう。

### 1. Chromaticのエラー
エラー文を読んでみてください。

![スクリーンショット 2025-04-04 2 32 40](https://github.com/user-attachments/assets/2cdfa45a-b6ce-448c-ac4d-60ae7cbdd71c)

読むとTodoCardのuseContext周りでエラーが出ていることがわかる。これはどういうエラーかというと、ローカルのStorybookでも表示されていたけど、**Hooks can only be called inside of the body of a function component.**というエラーだね。つまり、Hooksはfunctionの中で呼び出せってこと。
という観点でTodoCard/index.jsxを見てみると、`useAlertHandlerContext()`を呼び出している位置がおかしいことが見えてくるのではないでしょうか？
同様に、AlertManagerの方も修正してほしい！

### 2. contextValueの渡し方
現状、contextValueは下のように渡されているね。
```
<AlertHandlerContext.Provider value={{contextValue}}>
  {children}
</AlertHandlerContext.Provider>
```
こうやって渡すと、value={{contextValue}} → { contextValue: contextValue } という新しいネストしたオブジェクトを渡すということになる。よって、他のファイルでimportして使う時は、AlertHandlerContext.contextValue.setAlertのように呼び出さないといけなくなる。

よって、value={contextValue} → contextValueをそのまま渡せば、AlertHandlerContext.setAlertでアクセスできるようになります。
author_association: NONE
user: login: harada-0
created_at: 2025-04-04T05:00:56Z
body: 要点とエラーの原因が明確でわかりやすかったです。ありがとうございます。修正してみました。確認よろしくお願いします。
author_association: COLLABORATOR
user: login: luck-tech
created_at: 2025-04-04T05:30:38Z
body: 良い感じだね！あとはalertの幅を治してほしいかな。
fixedとfillの違いだね。

![スクリーンショット 2025-04-04 14 29 02](https://github.com/user-attachments/assets/530f6463-9158-4e65-bd6a-fb179e508b73)

![スクリーンショット 2025-04-04 14 29 12](https://github.com/user-attachments/assets/848496fd-1cb6-4584-83df-d56743033bfd)

author_association: NONE
user: login: luck-tech
created_at: 2025-04-04T16:33:13Z
body: LGTM！
お疲れ様！
author_association: NONE