TODOアプリにcss(TailwindCSS)を適用

# はじめに
ReactとTypeScriptで作成したTODOアプリにcss(TailwindCSS)を適用しました。
今回は適用した、cssおよびTailWindCssの導入方法や使い方等を記します。
# 前準備
今回利用するTODOアプリは以下の記事を参考に作成してください。

バックエンド（API作成）
[**DockerとLaravel10でAPI作成（Postmanで動作確認）**](https://qiita.com/91works-i-kato/items/73ff6e70950c93d79b61)

https://qiita.com/91works-i-kato/items/73ff6e70950c93d79b61

フロントエンド（環境構築）
[**Docker + React + TypeScript の 環境構築方法**](https://qiita.com/91works-i-kato/items/9f9ad03fee32d42ed547)

https://qiita.com/91works-i-kato/items/9f9ad03fee32d42ed547

フロントエンド（アプリ作成）
[**React＋TypeScript＋axiosでTODOアプリ作成（Docker環境）**](https://qiita.com/91works-i-kato/private/5fc7e06e50cf4abacf5a)

https://qiita.com/91works-i-kato/private/5fc7e06e50cf4abacf5a

# TailwindCSS導入方法
下記コマンドを実行してTailwindCSSインストールします。
```
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
```
tailwind.config.jsを以下に書き換える。

<details><summary>tailwind.config.jsコード</summary>

```tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```
</details>

フロントエンドプロジェクト内のsrcフォルダ下にあるindex.cssファイルの先頭に以下を追加
```index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
上記のindex.cssファイルから実際に適用するcssファイルを下記コマンドで作成します。
```
npx tailwindcss -i ./src/index.css -o ./src/components/output.css
```
フロントエンドプロジェクト内のsrcフォルダ下にあるcomponentsフォルダ下のApp.tsxファイルに、下記文を追加します。

<details><summary>App.tsxコード追加部</summary>

```diff:App.tsx
import { useRef } from "react";

import { useTodo } from "../hooks/useTodo";
import { Todo } from "../types/Todo";
import { TodoTitle } from "./TodoTitle";
import { TodoAdd } from "./TodoAdd";
import { TodoList } from "./TodoList";

+ import './output.css';

function App() {
  const { todoList, updateTodoListItem, addTodoListItem, deleteTodoListItem } = useTodo();

  const inputEl = useRef<HTMLTextAreaElement>(null);

  const handleAddTodoListItem = () => {
    if (inputEl.current?.value === "") {
      return;
    }
    addTodoListItem(inputEl.current!.value);
    inputEl.current!.value = "";
  };

  const incompletedList = todoList.filter((todo: Todo) => todo.text);


  return (
    <>
  <TodoTitle title="TODOアプリ" as="h1" />
  <TodoAdd buttonText="追加" inputEl={inputEl} handleAddTodoListItem={handleAddTodoListItem} />
  <TodoList todoList={incompletedList} updateTodoListItem={updateTodoListItem} deleteTodoListItem={deleteTodoListItem} title="TODOリスト" as="h2" />
  </>
  );
}

export default App;
```
</details>

# css適用部
### React時の注意事項
Reactではclassを指定する際、下記のようにclassNameを使用します。
```React時
className=""
```
### css適用後画面

通常画面
![css画像01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3353594/8b32caef-1e97-df24-ab2d-cb6d88079e5c.png)

更新時画面
![css画像07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3353594/85df4df5-f14b-9a36-855b-17ee8fa0e861.png)


### 1.TailWindCSS以外のcss部
##### ①背景色をグラデーションにして色をアニメーションで変化させる。
index.cssにコードを追加する。
<details><summary>index.cssコード追加部</summary>

```diff:index.css
@tailwind base;
@tailwind components;
@tailwind utilities;


body {
+  background: linear-gradient(45deg, #6cb8ff, #fff66c, #ffa36c);
+  background-size: 600% 600%;
+  animation: AnimationName 30s ease infinite;

  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

+ @keyframes AnimationName {
+  0%{background-position:0% 50%}
+  50%{background-position:100% 50%}
+  100%{background-position:0% 50%}
}
```
</details>

これで、背景色のグラデーションが時間とともに変化していきます。

##### ②文字やボタンに影をつけて立体的にする。
compornentsフォルダ内に3D.cssファイルを作成し、以下のコードを入力します。

<details><summary>3D.cssコード</summary>

```3D.css
.text-3d {
    text-shadow: 1px 1px 0 #000,
                 1.2px 1.2px 0 #000,
                 1.5px 1.5px 0 #000;
}
.shadow-md {
    box-shadow: 4px 6px 8px -2px rgba(0, 0, 0, 0.8), 4px 4px 6px -2px rgba(0, 0, 0, 0.6);
}
```
</details>

3D.cssファイルをApp.tsxファイルにimportします。

<details><summary>App.tsxコード追加部</summary>

```diff:App.tsx
import { useRef } from "react";

import { useTodo } from "../hooks/useTodo";
import { Todo } from "../types/Todo";
import { TodoTitle } from "./TodoTitle";
import { TodoAdd } from "./TodoAdd";
import { TodoList } from "./TodoList";

import './output.css';
+ import './3D.css';

function App() {
  const { todoList, updateTodoListItem, addTodoListItem, deleteTodoListItem } = useTodo();

  const inputEl = useRef<HTMLTextAreaElement>(null);

  const handleAddTodoListItem = () => {
    if (inputEl.current?.value === "") {
      return;
    }
    addTodoListItem(inputEl.current!.value);
    inputEl.current!.value = "";
  };

  const incompletedList = todoList.filter((todo: Todo) => todo.text);


  return (
    <>
  <TodoTitle title="TODOアプリ" as="h1" />
  <TodoAdd buttonText="追加" inputEl={inputEl} handleAddTodoListItem={handleAddTodoListItem} />
  <TodoList todoList={incompletedList} updateTodoListItem={updateTodoListItem} deleteTodoListItem={deleteTodoListItem} title="TODOリスト" as="h2" />
  </>
  );
}

export default App;
```
</details>

これで、文字を３Dにしたい時は、text-3dを、影を付けたい時は、shadow-mdを指定することができます。

### 2.TailwindCSS部

##### ①タイトル部
![css画像02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3353594/e43550ae-b373-59ac-e341-5e0a9f05b178.png)


![css画像03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3353594/b8819fe7-ac6c-7457-8d0f-d89f22a306aa.png)



<details><summary>TodoTitle.tsxコード変更部</summary>

```diff:TodoTitle.tsx
import { memo } from "react";

export const TodoTitle = memo(({ title, as }: { title: string; as: string}) => {
  if (as === "h1") {
+    return <h1 className="text-8xl font-extrabold text-center p-4 m-4 font-mono italic text-green-800 antialiased text-shadow-xs" style={{textShadow: '0 0 6px rgba(0,0,0,0.5)'}}>{title}</h1>;
  } else if (as === "h2") {
+    return <h2 className="text-6xl font-extrabold text-center p-2 m-2 font-mono italic text-green-800 antialiased text-shadow-xs" style={{textShadow: '0 0 6px rgba(0,0,0,0.5)'}}>{title}</h2>;
  } else {
    return <p>{title}</p>;
  }
});
```
</details>

##### ②TODO追加部
![css画像04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3353594/be03aa93-ea1e-9081-0084-7acf30f1c3de.png)

<details><summary>TodoAdd.tsxコード変更部</summary>

```diff:TodoAdd.tsx
import { RefObject } from "react";

export const TodoAdd = ({ buttonText, inputEl, handleAddTodoListItem }: { buttonText: string; inputEl: RefObject<HTMLTextAreaElement>; handleAddTodoListItem: () => void }) => {
  return (
+    <div className="flex justify-center w-full">
+      <textarea ref={inputEl} className="h-10 w-1/2 border rounded-lg bg-red-200 m-4 p-1 text-lg"/>
+      <button onClick={handleAddTodoListItem} className="m-2 flex w-full justify-center items-center px-0 py-3 text-base font-bold rounded-full shadow-md text-white bg-green-600 md:inline-flex md:w-auto md:px-12 hover:shadow-sm hover:text-black hover:bg-gradient-to-b from-blue-300 to-blue-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white focus:ring-green-700 dark:ring-offset-gray-900 dark:focus:ring-red-400 antialiased">{buttonText}</button>
    </div>
  );
};
```
</details>

##### ③TODOリスト表題部
![css画像05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3353594/6d635d67-99fc-84d9-e467-6736f4305e35.png)



<details><summary>TodoList.tsxコード変更部</summary>

```diff:TodoList.tsx
import { TodoTitle } from "./TodoTitle";
import { TodoItem } from "./TodoItem";
import { Todo } from "../types/Todo";

export const TodoList = ({
  todoList,
  updateTodoListItem,
  deleteTodoListItem,
  title,
  as,
}: {
  todoList: Todo[];
  updateTodoListItem: (id: string,value: string) => void;
  deleteTodoListItem: (id: string) => void;
  title: string;
  as: string;
}) => {
  return (
    <>
      {todoList.length !== 0 && (
        <>
        <TodoTitle title={title} as={as} />
+        <div className="flex justify-center">
          
+          <table className="font-mono font-extrabold italic text-green-800 text-center text-2xl m-4 p-4 border border-green-900 border-2 bg-gradient-to-b from-blue-200 to-blue-100">
+            <tr className="text-3d font-mono font-extrabold italic text-green-800 text-2xl m-8 p-4 border border-green-900 border-2">
+              <th className="font-extrabold text-center text-4xl m-4 p-4 border border-green-900 border-2">ＮＯ</th><th className="font-extrabold text-center text-4xl m-4 p-4 border border-green-900 border-2">　　　　　ＴＯＤＯ　内容　　　　　</th><th className="font-extrabold text-center text-4xl m-4 p-4 border border-green-900 border-2">ＴＯＤＯ追加更新日時</th><th className="font-extrabold text-center text-4xl m-4 p-6 border border-green-900 border-2">　更新　</th><th className="font-extrabold text-center text-4xl m-4 p-4 border border-green-900 border-2">　削除　</th>
            </tr>
            {todoList.map((todo) => (
              <tr key={todo.id}>
                <TodoItem todo={todo} key={todo.id} updateTodoListItem={updateTodoListItem} deleteTodoListItem={deleteTodoListItem} />
              </tr>
            ))}
          </table>
        </div>
        </>
      )}
    </>
  );
};

```
</details>

##### ④TODO部
更新ボタン無し時
![css画像06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3353594/db262e63-80db-d22e-2013-b364e8918cdb.png)


更新ボタン有り時
![css画像08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3353594/8cc8d05c-c680-29b0-59b2-010cfd9d0ed4.png)

<details><summary>TodoItem.tsxコード変更部</summary>

```diff:TodoItem.tsx
import React,{useState} from "react";
import { Todo } from "../types/Todo";

export const TodoItem = ({ todo, updateTodoListItem, deleteTodoListItem }: { todo: Todo; updateTodoListItem: any; deleteTodoListItem: any }) => {
  
  const [value, setValue] = useState("");
  const [disabled, setDisabled] = useState(true);
  const [active, setActive] = useState(false);

  const handleChangeText = (event: { target: { value: React.SetStateAction<string>; }; }) => {
    if(event.target.value===""){
      setDisabled(true);
      setActive(false);  
    }else{
      setDisabled(false);
      setActive(true);
      setValue(event.target.value);
    }
  };
  
  const handleUpdateTodoListItem = () =>{
    updateTodoListItem(todo.id,value);
    setDisabled(true);
    setActive(false);
  }
  const handleDeleteTodoListItem = () => deleteTodoListItem(todo.id);

  const year = todo.updated_at.substring(0,4);
  const month = todo.updated_at.substring(5,7);
  const day = todo.updated_at.substring(8,10);
  const hour = String(Number(todo.updated_at.substring(11,13)) + 9);
  const minute = todo.updated_at.substring(14,16);
  const second = todo.updated_at.substring(17,19);
  const datetime = year+"年"+month+"月"+day+"日"+hour+"時"+minute+"分"+second+"秒"

  return (
    <>
+      <td className="text-3d text-center text-2xl mb-8 p-1 pb-2 border border-green-900 border-2">{todo.id}</td>
+      <td className="text-center text-2xl mb-8 p-1 pb-2 border border-green-900 border-2"><input type="text" defaultValue={todo.text} onChange={handleChangeText} className="w-full p-1 pl-6 rounded-lg"/></td>
+      <td className="text-center text-2xl mb-8 p-1 pb-2 border border-green-900 border-2"><input type="text" value={datetime} className="w-full p-1 pl-6 rounded-lg"/></td>
+      <td className="text-center text-2xl mb-8 p-1 pb-2 border border-green-900 border-2"><button id={todo.id} onClick={handleUpdateTodoListItem} disabled={disabled} className={!active ? "" : "flex w-full justify-center items-center px-0 py-3 text-base font-bold rounded-full shadow-md text-white bg-purple-500 md:inline-flex md:w-auto md:px-12 hover:shadow-sm hover:bg-gradient-to-b hover:from-purple-600 hover:to-purple-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white focus:ring-purple-700 dark:ring-offset-gray-900 dark:focus:ring-red-400"}>{!active ? "":"更新"}</button></td>
+      <td className="text-center text-2xl mb-8 p-1 pb-2 border border-green-900 border-2"><button type="button" onClick={handleDeleteTodoListItem} className="flex w-full justify-center items-center px-0 py-3 text-base font-bold rounded-full shadow-md text-white bg-gray-500 md:inline-flex md:w-auto md:px-12 hover:shadow-sm hover:bg-gradient-to-b hover:from-gray-600 hover:to-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white focus:ring-gray-600 dark:ring-offset-gray-900 dark:focus:ring-red-400">削除</button></td>
    </>
  );
};
```
</details>


# クラスの調べ方

公式サイトやチートシートサイトを参考にしてください。

TailwindCSSの公式サイトになります。

https://tailwindcss.com/docs/installation

TailwindCSSのチートシートになります。

https://tailwindcomponents.com/cheatsheet/

# 最後に
最後まで読んで頂きましてありがとうございました。