はじめに
今回は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配下に各種フォルダとファイルを作成しました。
/src/components
import React from "react";
const Footer:React.FC = () => (
<footer>
<p>2023 trelloクローンアプリ制作</p>
</footer>
);
export default Footer;
/src/pages
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を採用しました。
使用方法は公式リファレンスを確認してください。
Material UIでは、CSSに記述をすることなくindex.tsx内でCSSのように記述ができます。
また、アイコンも豊富に用意されており使いやすく助かりました。
例えば、今回はdivタグの役割を継承した、ActionAreaを作成し
const ActionArea = styled("div")({
display: "flex",
alignItems: "center",
marginBottom: "16px",
});
JSX内でdivタグをと同じようにActionAreaを使います。
新規作成の欄を調節するために作成しました。
<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>
"dev": "next dev -p 8080",
package.jsonについて、docker-composeで設定したポートを指定するようにしてください
前回の記事で作成したAPIからデータを取得するため、APIで指定したURLをfetchしています。
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しています。
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下記のように表示されるかと思います。
では、この状態でhttp://localhost:8080/
にアクセスしてみると下記のGifのように動作します。
機能として、簡単に2点となります。
新規作成の入力タブからのToDoリスト作成、リスト名称の右側に配置している削除ボタンからのリスト削除です。
以上で、ToDoリストの完成です。
最後に
最終的にはここからさらに発展させ、trelloクローンを作成させるつもりです。
trelloであれば、今回作成したToDoリストをボードと見立て、リンクを付与し、ページ遷移、そこからカード作成、リスト作成と発展させていく予定です。