Amplify Gen2のドキュメントを見ながらToDoアプリを作成してみた

どうも駆け出し12冠エンジニアのアスカです。今日はAmplify Gen2の触りに、[ドキュメント](https://docs.amplify.aws/gen2/)を見ながらToDoアプリを作成してみたので、その内容を共有したいと思います。
ドキュメントはToDo項目の作成までですが、生成系AIの助けも借りつつ編集と削除も実装しました。
![Qiita](https://d39oe90ddnxn3x.cloudfront.net/amplify-gen2-doc-tutorial/amplify-gen2-doc-tutorial2.png)
Amlify Gen2のワークショップから入ろうとして挫折した経緯もあるので、Amplifyへの入門を考えている方はこちらも見てみてください。
[[忘備録]Amplify Gen2 Workshopの詰まり所](https://qiita.com/asuka0708japan/items/4a6d757a34b3fd127799)

# Todoアプリの作成
ドキュメントに沿ったToDoアプリ作成の流れは

1. フレームワークの選定
1. プロジェクトの作成
1. バックエンドの作成
1. バックエンドのビルド
1. UIの作成

になります。おまけで機能拡張を行いました。

## フレームワークの選定
ドキュメントのQuickStartからフレームワークを選定します。
自分はこれらを明確に理由を付けて使い分けが出来ないので、利用経験がある、かつ、好評のVite+Reactで作成しました。
![](https://d39oe90ddnxn3x.cloudfront.net/amplify-gen2-doc-tutorial/amplify-gen2-doc-tutorial1.png)
## プロジェクトの作成
まずは事前準備が必要な環境が記載されています。
- Node.js v18.17 or later
- npm v9 or later
- git v2.14.1 or later

ただし、Node.jsに関しては最新の20では不具合が発生するので注意してください。
参考:[[忘備録]Amplify Gen2 Workshopの詰まり所](https://qiita.com/asuka0708japan/items/4a6d757a34b3fd127799)


プロジェクトを開始するには、作成したいディレクトリで以下のコマンドを実行します。
```
npm create vite@latest react-amplify-gen2 -- --template react-ts
```
react-amplify-gen2という名前のフォルダが作成されるので、必要に応じて書き換えてください。
プロジェクトディレクトリに移動し、依存関係をinstallします。
```
cd react-amplify-gen2
npm install
```

## バックエンドの作成
以下のコマンドでバックエンドのファイル構成が作成されます。
```
npm create amplify@latest
```
ファイル構成は以下のようになっています。
```
├── amplify/
│   ├── auth/
│   │   └── resource.ts
│   ├── data/
│   │   └── resource.ts
│   ├── backend.ts
│   └── package.json
├── node_modules/
├── .gitignore
├── package-lock.json
├── package.json
└── tsconfig.json
```
この状態でローカルサーバーとAmplifyのサンドボックス環境を立ち上げて開発を進めていきます。
```
npm run dev
```
```
npx amplify sandbox
```
ローカルサーバを立てたままにする事によって、フロントエンドへの変更はViteによって自動で反映されます。
また、Amplifyサンドボックス環境はバックエンドの変更を監視して自動で反映します。

次にデータスキーマを定義していきます。amplify/data/resource.tsを編集して、ToDoリストの内容、重要度、終わったかどうかを記載出来るようにします。また認証にはCognitoユーザプールを使用するよう設定します。
```typescript:amplify/data/resource.ts
import { type ClientSchema, a, defineData } from '@aws-amplify/backend';

const schema = a.schema({
  Todo: a
    .model({
      content: a.string(),
      done: a.boolean(),
      priority: a.enum(['low', 'medium', 'high'])
    })
    .authorization(allow => [allow.owner()]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool'
  }
});
```

次にamplify/auth/resource.tsを編集して認証機能を追加していきます。
```typescript: amplify/auth/resource.ts
import { defineAuth } from '@aws-amplify/backend';

export const auth = defineAuth({
  loginWith: {

   email: {
     verificationEmailSubject: 'Welcome! Verify your email!'
   },
  }
});
```
最後にAmplify経由で認証機能とデータを使用出来るよう設定をします。
```typescript: amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { data } from './data/resource';

defineBackend({
  auth,
  data
});
```
## UIの作成
ここから作成したバックエンドを利用してUI部分を作っていきます。
src/components配下にTodoList.tsxを作成し、以下のコードを記述します。
```typescript:src/components/TodoList.tsx
import { useState, useEffect } from "react";
import { generateClient } from "aws-amplify/data";
import type { Schema } from "../../amplify/data/resource";

export default function TodoList() {
  // generate your data client using the Schema from your backend
  const client = generateClient<Schema>();

  const [todos, setTodos] = useState<Schema["Todo"][]>([]);

  async function listTodos() {
    // fetch all todos
    const { data } = await client.models.Todo.list();
    setTodos(data);
  }

  useEffect(() => {
    listTodos()
    const sub = client.models.Todo.observeQuery().subscribe(({ items }) =>
     setTodos([...items])
    );
  
    return () => sub.unsubscribe();
  }, []);

  return (
    <div>
      <h1>Todos</h1>
      <button onClick={async () => {
        // create a new Todo with the following attributes
        const { errors, data: newTodo } = await client.models.Todo.create({
          // prompt the user to enter the title
          content: window.prompt("title"),
          done: false,
          priority: 'medium'
        })
        console.log(errors, newTodo);
      }}>Create</button>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.content}</li>
        ))}
      </ul>
    </div>
  );
}
```
これで、新しい項目を作成出来るTodoListを作成出来ました。"Create"ボタンから項目を作成し、useEffectにより項目が更新されたらリストを取得しなおして画面の更新をします。
最後にsrc/App.tsxに以下のコードを記述します。
```typescript: src/App.tsx
import { withAuthenticator } from "@aws-amplify/ui-react";
import "@aws-amplify/ui-react/styles.css";
import TodoList from "./components/TodoList";

function App() {
  return (
    <>
      <h1>Hello, Amplify 👋</h1>
      <TodoList />
    </>
  );
}

export default withAuthenticator(App);
```
TodoListを読み込み、export default withAuthenticator(App);で認証機能を追加しています。
これでリポジトリに上げてホスティングする所までがドキュメントの流れです。
## 機能拡張
現在の状態ではTodoアプリとしてもの足りない点が複数箇所あると思います。
- 作成時に重要度、doneの指定が出来ない。
- 作成した項目の編集、削除が出来ない。
- 状態に応じた表示の変更が出来ない。
今回は上記の点の修正をかけたいと考えています。
### 作成時に重要度、doneの指定
これを指定するのはcreate関数になります。以下のようなcreateTodo関数を定義して、ボタン押下時に実行されるようにします。
```typescript: src/components/TodoList.tsx
(略)
async function createTodo() {
    const content = window.prompt("Enter the todo title:");
    const done = window.confirm("Is the task completed?");
    const priority = window.prompt("Set the priority (low, medium, high):", "medium");
    if (content) {
      const { errors, data: newTodo } = await client.models.Todo.create({
        content,
        done,
        priority
      });
      console.log(errors, newTodo);
      listTodos();
    }
  }
(略)
 return (
    <div>
      <button onClick={createTodo}>Create</button>
    (略)
    </div>
  );
}

```
createTodo関数では作成時にdoneやpriorityの値も編集出来るようにしました。

### 作成した項目の編集、削除が出来るようにする。
こちらもそれぞれ項目の編集と削除に対応する関数を用意してボタンを作成します。
```typescript: src/components/TodoList.tsx
(略)
async function deleteTodo(todo) {
    await client.models.Todo.delete(todo);
    // 更新されたTodoリストを取得
    listTodos();
  }
  async function editTodo(todo) {
    const newContent = window.prompt("Edit your todo content:", todo.content);
    const newDone = window.confirm("Is the task completed?");
    const newPriority = window.prompt("Set the priority (low, medium, high):", todo.priority);
    
    if (newContent !== null || newPriority) {
      await client.models.Todo.update({
        id: todo.id,
        content: newContent,
        done: newDone,
        priority: newPriority
      });
      listTodos();
    }
  }
  (略)
  return (
  <div>
  (略)
  <ul>
        {todos.map((todo) => (
          <li key={todo.id} className="todo-item" style={{
          }}>
              {todo.content} - {todo.done ? "Done" : "Pending"} - {todo.priority}
            <button className="todo-button" onClick={() => editTodo(todo)}>Edit</button>
            <button className="todo-button" onClick={() => deleteTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
  </div>
  );
```

### 状態に応じた表示の修正
見た目の部分を修正していきます。
- doneの状態がtrueであれば、項目の背景をグレー、falseであれば黄色
- 重要度がhighは赤、mediumは緑、lowは青に文字色を設定
それぞれ2つの関数を設定して色付けしていきます。
```typescript: src/components/TodoList.tsx
function getPriorityColor(priority) {
    switch (priority) {
      case 'high':
        return 'red'; // 高優先度は赤色
      case 'medium':
        return 'green'; // 中優先度は緑色
      case 'low':
        return 'blue'; // 低優先度は青色
      default:
        return 'black'; // 優先度が設定されていない場合は黒色
    }
  }

  function getDoneBackgroundColor(done) {
    return done ? 'grey' : 'lightyellow'; // 完了済みはグレー、未完了は薄い黄色
  }
```

```diff_typescript: src/components/TodoList.tsx
return (
    <div>
      <button onClick={createTodo}>Create</button>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id} className="todo-item" style={{
+            color: getPriorityColor(todo.priority),
+            backgroundColor: getDoneBackgroundColor(todo.done) // 背景色の設定
          }}>
              {todo.content} - {todo.done ? "Done" : "Pending"} - {todo.priority}
            <button className="todo-button" onClick={() => editTodo(todo)}>Edit</button>
            <button className="todo-button" onClick={() => deleteTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}
```

これで多少ましなTodoアプリになったかなと思います。

# まとめ
今回はドキュメントを見ながらTodoアプリの作成をして、物足りない部分の修正、機能追加を行いました。Amplifyかなり便利ですね。使いこなせるようになっていきたいです。