LoginSignup
0
1

React、TypeScript、Next.js+APIを使用してToDoリストを作成してみた

Last updated at Posted at 2023-07-20

はじめに

今回はReact、TypeScript、Next.jsを使用してToDoリストを作成したいと思います。
この記事を書くにあたり、Trelloをモデルとしたクローンアプリの作成の経過の中で使用したことを記述しております。
バックエンド側となるAPIの作成については、下記記事をご覧ください。

Node.js+MySQL+Dockerを使用したAPIの作成
https://qiita.com/t_k_t/items/93567a7ebe912da575c7

環境

MacBook M1
React
TypeScript
Next.js
Material UI
Docker

1.環境構築

Dockerを使用しての環境構築となりますので、docker-composeを下記のように記述します。
前回記事の内容に追記する形で記述していきます。


services:
  db:
    image: mysql:8.0.33
    #Macbook m1を使用しているためplantformを指定
    platform: linux/amd64
    container_name: mysql
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_USER: ユーザ名
      MYSQL_PASSWORD: パスワード
      MYSQL_DATABASE: trello
      TZ: "Asia/Tokyo"
    volumes:
      - db-data:/var/lib/mysql
    ports:
      - 3307:3306

  backend:
    image: node:18-slim
    volumes:
      - ./backend:/usr/src/app
    working_dir: /usr/src/app
    command: bash -c "npm install && npm start"
    ports:
      - 3000:3000
    depends_on:
      - db
    #URLはlocalhostではなく、docker-composeで指定しているservicesのdbを指定
    environment:
      DATABASE_URL: mysql://ユーザー名:パスワード@db:3306/trello

+  frontend:
+    build: ./frontend
+    volumes:
+      - ./frontend:/usr/src/app
+      - /app/node_modules
+    working_dir: /usr/src/app
+    ports:
+      - 8080:8080
+    depends_on:
+      - backend

volumes:
  db-data:

ルートディレクトリにて、下記コマンドを実行しプロジェクトを作成します。
今回名称はfrontendとします。

npx create-next-app@latest --typescript

作成したfrontend直下にdockerfileを作成します。


FROM node:18-slim

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 8080

CMD [ "npm", "run", "dev" ]

実装

実装にあたり不要なファイルであるプロジェクト作成の際に自動生成される/src配下を全て削除します。

削除後、下記画像のように/src配下に各種フォルダとファイルを作成しました。

スクリーンショット 2023-07-19 15.32.36.png

/src/components

footer.tsx

import React from "react";

const Footer:React.FC = () => (
    <footer>
        <p>2023 trelloクローンアプリ制作</p>
    </footer>
);

export default Footer;

/src/pages

index.tsx
import Link from "next/link";
import Footer from "@/components/footer";
import { useEffect, useState } from "react";
import {
  AppBar,
  Button,
  Card,
  CardContent,
  CardHeader,
  IconButton,
  MenuItem,
  TextField,
  ThemeProvider,
  Toolbar,
  Typography,
  createTheme,
  makeStyles,
  styled,
} from "@mui/material";

import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import CancelPresentationIcon from "@mui/icons-material/CancelPresentation";

declare module "@mui/material/styles" {
  interface Theme {
    status: {
      danger: React.CSSProperties["color"];
    };
  }
  interface ThemeOptions {
    status: {
      danger: React.CSSProperties["color"];
    };
  }
  interface Palette {
    neutral: Palette["primary"];
  }
  interface PaletteOptions {
    neutral: PaletteOptions["primary"];
  }
  interface PaletteColor {
    darker?: string;
  }
  interface SimplePaletteColorOptions {
    darker?: string;
  }
}

interface Board {
  id: number;
  name: string;
  board_order: number;
}

const theme = createTheme({
  status: {
    danger: "#64c385",
  },
  palette: {
    primary: {
      main: "#6ae2d0",
      darker: "#6f8a97",
    },
    neutral: {
      main: "#07da8d",
      contrastText: "#fff",
    },
    error: {
      main: "#bced8f",
    },
  },
});

const ActionArea = styled("div")({
  display: "flex",
  alignItems: "center",
  marginBottom: "16px",
});

const StyledTextField = styled("div")({
  display: "flex",
  alignItems: "center",
  marginBottom: "16px",
});

const StyledCard = styled(Card)({
  backgroundColor: "#f5f5f5",
  margin: "16px",
});

const CardContainer = styled("div")({
  display: "flex",
  flexWrap: "wrap",
  justifyContent: "space-second",
});

const CardContentStyled = styled(CardContent)({
  position: "relative",
});

const DeleteButton = styled(Button)({
  backgroundColor: theme.palette.error.main,
  width: "20px",
  height: "20px",
  margin: "16px",
  color: "white",
  "&:hover": {
    backgroundColor: theme.palette.error.dark,
  },
});

export default function Home() {
  const [boards, setBoards] = useState<Board[]>([]);
  const [createBoard, setCreatBoard] = useState("");

  const classes = useState();

  const getBoards = () => {
    fetch("http://0.0.0.0:3000/boards")
      .then((response) => response.json())
      .then((data) => {
        setBoards(
          data.sort((a: Board, b: Board) => a.board_order - b.board_order)
        );
      })
      .catch((error) => {
        console.log(error);
      });
  };

  useEffect(() => {
    getBoards();
  }, []);

  const handleCreateBoard = () => {
    let createBoardOrder: number =
      boards.length === 0
        ? 1
        : Math.max(...boards.map((board) => board.board_order)) + 1;

    fetch("http://0.0.0.0:3000/boards", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        name: createBoard,
        board_order: createBoardOrder,
      }),
    })
      .then((response) => {
        if (!response.ok) {
          throw new Error("レスポンスエラー");
        }
        getBoards();
        setCreatBoard("");
      })
      .catch((error) => {
        console.log(error);
      });
  };

  const handleDeleteBoard = (id: number) => {
    fetch(`http://0.0.0.0:3000/boards/${id}`, {
      method: "DELETE",
    })
      .then(() => {
        setBoards(boards.filter((board) => board.id !== id));
      })
      .catch((error) => {
        console.log(error);
      });
  };

  return (
    <div>
      <ThemeProvider theme={theme}>
        <AppBar position="static">
          <Toolbar>
            <IconButton edge="start" color="inherit">
              <MenuItem />
            </IconButton>
            <Typography variant="h6">trello clone</Typography>
          </Toolbar>
        </AppBar>
        <main>
          <ActionArea>
            <StyledTextField>
              <TextField
                label="新規作成"
                type="text"
                value={createBoard}
                margin="normal"
                onChange={(e) => setCreatBoard(e.target.value)}
              />
              <Button
                variant="contained"
                color="primary"
                onClick={handleCreateBoard}
              >
                <AddCircleOutlineIcon />
              </Button>
            </StyledTextField>
          </ActionArea>
          <CardContainer>
            {boards.map((board) => (
              <StyledCard key={board.id}>
                <CardHeader
                  title={board.name}
                  action={
                    <DeleteButton
                      variant="contained"
                      size="small"
                      onClick={() => handleDeleteBoard(board.id)}
                    >
                      <CancelPresentationIcon />
                    </DeleteButton>
                  }
                />
              </StyledCard>
            ))}
          </CardContainer>
        </main>
        <Footer />
      </ThemeProvider>
    </div>
  );
}


今回は、Material UIを採用しました。
使用方法は公式リファレンスを確認してください。

https://mui.com/

Material UIでは、CSSに記述をすることなくindex.tsx内でCSSのように記述ができます。
また、アイコンも豊富に用意されており使いやすく助かりました。

例えば、今回はdivタグの役割を継承した、ActionAreaを作成し

 index.tsx
const ActionArea = styled("div")({
  display: "flex",
  alignItems: "center",
  marginBottom: "16px",
});

JSX内でdivタグをと同じようにActionAreaを使います。
新規作成の欄を調節するために作成しました。

index.tsx

           <ActionArea>
            <StyledTextField>
              <TextField
                label="新規作成"
                type="text"
                value={createBoard}
                margin="normal"
                onChange={(e) => setCreatBoard(e.target.value)}
              />
              <Button
                variant="contained"
                color="primary"
                onClick={handleCreateBoard}
              >
                <AddCircleOutlineIcon />
              </Button>
            </StyledTextField>
          </ActionArea>

package.json

    "dev": "next dev -p 8080",

package.jsonについて、docker-composeで設定したポートを指定するようにしてください

前回の記事で作成したAPIからデータを取得するため、APIで指定したURLをfetchしています。

index.tsx
const getBoards = () => {
    fetch("http://0.0.0.0:3000/boards")
      .then((response) => response.json())
      .then((data) => {
        setBoards(
          data.sort((a: Board, b: Board) => a.board_order - b.board_order)
        );
      })
      .catch((error) => {
        console.log(error);
      });
  };

また、更新や削除の際は、methodやheader,bodyを状況に合わせ指定してfetchしています。

index.tsx

fetch("http://0.0.0.0:3000/boards", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        name: createBoard,
        board_order: createBoardOrder,
      }),
    })

以上全て完了しましたら、下記コマンドでDockerを起動していきます。

docker-compose up -d

起動が完了するとDocker Desktop下記のように表示されるかと思います。

スクリーンショット 2023-07-19 14.18.56.png

では、この状態でhttp://localhost:8080/
にアクセスしてみると下記のGifのように動作します。

タイトルなし.gif

機能として、簡単に2点となります。
新規作成の入力タブからのToDoリスト作成、リスト名称の右側に配置している削除ボタンからのリスト削除です。

スクリーンショット 2023-07-20 10.11.00.png

以上で、ToDoリストの完成です。

最後に

最終的にはここからさらに発展させ、trelloクローンを作成させるつもりです。
trelloであれば、今回作成したToDoリストをボードと見立て、リンクを付与し、ページ遷移、そこからカード作成、リスト作成と発展させていく予定です。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1