ノーコード ラボ

NoCode 関連のツールの紹介、使い方などをやさしく説明しています。

Create.xyz & Xano 連携!簡単なタスク管理アプリを作成してみよう

みなさん、こんにちは!今回は Create.xyz(以下 Create)と、バックエンド側の機能をノーコードで構築できる人気のサービス Xano を連携していきます。

Xano は過去の記事でも何度か取り上げてきましたが、本記事ではフロントエンド側を Create、バックエンド側を Xano で構築したアプリを作成していきます。なお、Xano については、以前の記事「ncScale を使ったノーコードアプリの管理 Xano 編」で構築したデータベースと API を再利用していきたいと思います。

前述の記事では、フロントエンド側を Bubble で作成していて、今回は Bubble の部分を Create に置き換えるイメージとなります。その為、Xano 側の構築についての詳細は、前述の記事も併せてご参考いただければと思います。

また、2024/9/24のアップデートで、チャットで対話しながらアプリを生成できるようになりました!基本はシンプルな応答ですが、要求すればしっかり説明もしてくれるので、確認と調整がしやすくなっています。今までと同じプロンプトモードも利用できます。(なお本記事はアップデート前のバージョンを使用して作成しています。)

1. 作成するアプリ

作成するアプリは、簡単なタスク管理を行うアプリです。サインアップ/ログインができて、タスク一覧の表示、タスクの新規作成、更新、削除を行うことができます。

2. Xano の設定

冒頭でお伝えした通り Xano のデータベースと API については、以下の記事を併せてご参考ください。

blog.nocodelab.jp

なお、簡単に補足しておきますと、Xano で構築しているのは以下の内容となっています。

2.1. データベース

データベーステーブルは user Tasks TaskStatuses の3つを用意します。

2.2. APIエンドポイント

認証で使用しているのは auth/login auth me auth signup の3つです。

タスク情報の CRUD(Create/Read/Update/Delete)関連のエンドポイントは5つです。

なお、今回のサンプルでは、タスク一覧を取得する際に Tasks テーブルのリレーション関係にある TaskStatuses テーブルの値も取得しているエンドポイント tasks_getTaskStatusName を追加で準備しましたので、そちらの設定だけ補足しておきたいと思います。

タスク情報(Tasks テーブル)は、ステータス情報(taskstatuses_id フィールド)を持っていて、このデータはステータス情報のマスタ(TaskStatuses テーブル)と id を基に紐づいてます。

Xano 上で確認すると、taskstatuses_id フィールドの値は「未完了」や「進行中」といった値が登録されているように見えますが、taskstatuses_id のデータ型は integer であるため、実際の値は数値で登録されています。

tasks_getTaskStatusName エンドポイントは「未完了」や「進行中」といったステータス名を、タスク情報と一緒に取得するためのもので、GET メソッドの tasks をコピーしたエンドポイントに対して Output に TaskStatuses テーブルの情報を追加しています。

Xano 側の構築については以上です。

3. Create の設定

次に Create でフロントエンドの作成を行っていきたいと思います。

3.1. ページ生成

Create で作成するページは以下の5ページです。

no ページタイトル ページ名(URL) 詳細
1 Login Page / ログインページ
2 Signup Page /signup サインアップページ
3 My Tasks Page /mytasks タスク一覧ページ
4 Task Creation Page /newtask タスク作成ページ
5 Task Editor Page /edittask タスク編集ページ

3.2. ユーザー認証について

今回のサンプルアプリでは、サインアップ/ログイン機能を実装しています。認証するユーザー情報に関しては Xano 側にある user テーブルを使用していますので、以前の記事でご紹介した Create のユーザーアカウント機能については使用していません。Create 内の機能を使ってユーザー認証を行いたい場合は、以下の記事をご参考くださいね。

blog.nocodelab.jp

また、認証については Xano API から受け取ったトークンを、ローカルストレージに保存する方法で実装しました。セキュリティ上、トークンをローカルストレージで管理することはベストプラクティスではありませんので、実運用を検討されている方はトークンを取得したらデータベースに保存するなど、より安全な方法をご検討ください。

なお、本アプリでは既存で準備していた Xano データベースを利用しますので、ローカルストレージに保存する形で進めます。

3.3. 各ページのプロンプトとコード

各ページの最終的なプロンプトと、生成されたコードは以下の通りです。ポイントとして、プロンプトでは変数名やページURLなどを、できるだけ具体的に記載することがスムーズな生成に繋がりました。

具体的には、クエリストリングやローカルストレージのキー値、またURLとなるページ名などプログラムのコードにした際に影響を受けそうな部分を固定で指定しています。

3.3.1. Login Page

ログインページは以下のようなプロンプトで作成しています。

メールアドレスを使用したログインページを作成。

入力フォームは以下の2つ
email
password

サインアップページへのリンクがあります。

デザインは以下のイメージ。
[ ログインページのイメージ画像]

Xano API を使ったログインを実行します。
エンドポイントは以下のURLを使用します。
[Xano API のログイン用エンドポイントURL]

ログインに成功したら、受け取ったトークンを authToken という名前のローカルストレージに保存して、mytasks ページに遷移します。
ログインに失敗したら、受け取ったエラーを表示します。

実際に生成されたコードは、以下のような内容となりました。

function MainComponent() {
  const [email, setEmail] = React.useState('');
  const [password, setPassword] = React.useState('');
  const [error, setError] = React.useState('');

  const handleLogin = async (e) => {
    e.preventDefault();
    setError('');

    try {
      const response = await fetch('Xano API のログイン用エンドポイントURL', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ email, password }),
      });

      const data = await response.json();

      if (response.ok) {
        localStorage.setItem('authToken', data.authToken);
        window.location.href = '/mytasks';
      } else {
        setError(data.message || 'ログインに失敗しました。');
      }
    } catch (err) {
      setError('ネットワークエラーが発生しました。');
    }
  };

  return (
    <div className="min-h-screen bg-gray-100 flex flex-col justify-center items-center font-sans">
      <div className="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
        <h1 className="text-2xl font-bold text-center mb-6">ログイン</h1>
        <form onSubmit={handleLogin}>
          <div className="mb-4">
            <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
              メールアドレス
            </label>
            <input
              type="email"
              id="email"
              name="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
              required
            />
          </div>
          <div className="mb-6">
            <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
              パスワード
            </label>
            <input
              type="password"
              id="password"
              name="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
              required
            />
          </div>
          {error && <p className="text-red-500 text-sm mb-4">{error}</p>}
          <button
            type="submit"
            className="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
          >
            ログイン
          </button>
        </form>
        <div className="mt-4 text-center">
          <p className="text-sm text-gray-600">アカウントをお持ちでない方</p>
          <a href="/signup" className="text-indigo-600 hover:text-indigo-800 font-medium">
            サインアップ
          </a>
        </div>
      </div>
    </div>
  );
}

3.3.2. Signup Page

サインアップページも、ログインページと同じような要領でプロンプトを指定しています。

メールアドレスを使用したサインアップページを作成。

入力フォームは以下3つ
name
email
password

ログインページへのリンクがあります。

デザインのイメージは以下。
[ サインアップページのイメージ画像]

Xano API を使ったサインアップを実行します。
エンドポイントは以下のURLを使用します。
[Xano API のサインアップ用エンドポイントURL]


サインアップに成功したら、受け取ったトークンを authToken という名前のローカルストレージに保存し、mytasks ページ遷移します。

サインアップに失敗したら、受け取ったエラーを表示します。

以下が実際に生成されたコードです。

function MainComponent() {
  const [error, setError] = React.useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const userData = Object.fromEntries(formData.entries());

    try {
      const response = await fetch('[Xano API のサインアップ用エンドポイントURL]', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(userData),
      });

      const data = await response.json();

      if (response.ok) {
        localStorage.setItem('authToken', data.authToken);
        setError('');
        window.location.href = '/mytasks';
      } else {
        setError(data.message || '不明なエラーが発生しました');
      }
    } catch (error) {
      setError('サインアップ中にエラーが発生しました');
    }
  };

  return (
    <div className="min-h-screen bg-gray-100 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
      <div className="sm:mx-auto sm:w-full sm:max-w-md">
        <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
          アカウントを作成
        </h2>
      </div>

      <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
        <div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
          <form className="space-y-6" onSubmit={handleSubmit}>
            <div>
              <label htmlFor="name" className="block text-sm font-medium text-gray-700">
                名前
              </label>
              <div className="mt-1">
                <input
                  id="name"
                  name="name"
                  type="text"
                  required
                  className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                />
              </div>
            </div>

            <div>
              <label htmlFor="email" className="block text-sm font-medium text-gray-700">
                メールアドレス
              </label>
              <div className="mt-1">
                <input
                  id="email"
                  name="email"
                  type="email"
                  autoComplete="email"
                  required
                  className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                />
              </div>
            </div>

            <div>
              <label htmlFor="password" className="block text-sm font-medium text-gray-700">
                パスワード
              </label>
              <div className="mt-1">
                <input
                  id="password"
                  name="password"
                  type="password"
                  autoComplete="new-password"
                  required
                  className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                />
              </div>
            </div>

            {error && (
              <div className="text-red-600 text-sm">{error}</div>
            )}

            <div>
              <button
                type="submit"
                className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-[#5850EC] hover:bg-[#4c45d4] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#5850EC]"
              >
                サインアップ
              </button>
            </div>
          </form>

          <div className="mt-6">
            <div className="relative">
              <div className="absolute inset-0 flex items-center">
                <div className="w-full border-t border-gray-300"></div>
              </div>
              <div className="relative flex justify-center text-sm">
                <span className="px-2 bg-white text-gray-500">
                  既にアカウントをお持ちですか?
                </span>
              </div>
            </div>

            <div className="mt-6">
              <a
                href="/login"
                className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-[#5850EC] bg-white hover:bg-gray-50"
              >
                ログイン
              </a>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}


3.3.3. My Tasks

My Tasks はタスク一覧ページです。ここでは、サインアップ/ログイン時に受け取ったトークンをローカルストレージから取り出してデータを取得しています。また、タスクの新規作成や編集画面への遷移、タスク削除時の処理についても指示しています。

mytasks ページを作成。
見出しは「タスク一覧」とします。
ユーザー情報は以下のエンドポイントを使用します。
[Xano API のユーザー情報用エンドポイントURL]

ユーザー情報の取得に必要なトークンはローカルストレージ authToken に保存されているトークンを使用します。
取得したユーザー情報の名前を画面に表示します。
以下のエンドポイントを使用して、ユーザーのタスク一覧を取得します。トークンは authToken を使用します。
[Xano API のタスク一覧用エンドポイントURL]

ユーザー名の右側に、タスク作成用のシンプルなアイコンを配置します。アイコンをクリックしたら、newtask ページに遷移します。

以下はレスポンスの定義です。参考にしてください。
[XanoのレスポンスJSONイメージ画像]

取得したタスク情報の行番号、タイトル、タスクステータス、締め切り、編集用アイコン、削除アイコンを表示します。なお、タスクステータスは、taskstatuses_id ではなく、_taskstatuses 配列1件目 の name の値を参照してください。
編集用アイコンがクリックされたら、選択しているタスク情報の id を taskid という名前のクエリストリングにセットして、edittask ページに遷移します。

削除アイコンをクリックしたら、以下のエンドポイントを使用して、タスク情報を削除します。
[Xano API のタスク削除用エンドポイントURL]

削除が完了したら、タスク一覧を更新します。

実際に生成されたコードは以下です。

function MainComponent() {
  const [user, setUser] = React.useState(null);
  const [tasks, setTasks] = React.useState([]);
  const [error, setError] = React.useState(null);

  const fetchTasks = async (token) => {
    try {
      const tasksResponse = await fetch(
        "[Xano API のタスク一覧用エンドポイントURL]",
        {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        }
      );

      if (!tasksResponse.ok) {
        throw new Error("タスク情報の取得に失敗しました。");
      }

      const tasksData = await tasksResponse.json();
      setTasks(tasksData);
    } catch (err) {
      setError(err.message);
    }
  };

  React.useEffect(() => {
    const fetchData = async () => {
      const token = localStorage.getItem("authToken");
      if (!token) {
        setError("トークンが見つかりません。ログインしてください。");
        return;
      }

      try {
        const userResponse = await fetch(
          "[Xano API のユーザー情報用エンドポイントURL]",
          {
            headers: {
              Authorization: `Bearer ${token}`,
            },
          }
        );

        if (!userResponse.ok) {
          throw new Error("ユーザー情報の取得に失敗しました。");
        }

        const userData = await userResponse.json();
        setUser(userData);

        await fetchTasks(token);
      } catch (err) {
        setError(err.message);
      }
    };

    fetchData();
  }, []);

  const handleEditClick = (taskId) => {
    window.location.href = `/edittask?taskid=${taskId}`;
  };

  const handleNewTaskClick = () => {
    window.location.href = "/newtask";
  };

  const handleDeleteClick = async (taskId) => {
    const token = localStorage.getItem("authToken");
    if (!token) {
      setError("トークンが見つかりません。ログインしてください。");
      return;
    }

    try {
      const response = await fetch(
        `[Xano API のタスク削除用エンドポイントURL]`,
        {
          method: 'DELETE',
          headers: {
            Authorization: `Bearer ${token}`,
          },
        }
      );

      if (!response.ok) {
        throw new Error("タスクの削除に失敗しました。");
      }

      await fetchTasks(token);
    } catch (err) {
      setError(err.message);
    }
  };

  return (
    <div className="min-h-screen bg-gray-100 py-6 flex flex-col justify-center sm:py-12">
      <div className="relative py-3 sm:max-w-4xl sm:mx-auto">
        <div className="absolute inset-0 bg-gradient-to-r from-cyan-400 to-light-blue-500 shadow-lg transform -skew-y-6 sm:skew-y-0 sm:-rotate-6 sm:rounded-3xl"></div>
        <div className="relative px-4 py-10 bg-white shadow-lg sm:rounded-3xl sm:p-20">
          <div className="max-w-3xl mx-auto">
            <div>
              <h1 className="text-2xl font-semibold text-center font-roboto mb-6">
                タスク一覧
              </h1>
            </div>
            <div className="divide-y divide-gray-200">
              {error ? (
                <p className="text-red-500 mt-4">{error}</p>
              ) : user ? (
                <div className="py-8 text-base leading-6 space-y-4 text-gray-700 sm:text-lg sm:leading-7">
                  <div className="flex justify-between items-center mb-4">
                    <p className="font-roboto">ようこそ、{user.name}さん</p>
                    <button
                      onClick={handleNewTaskClick}
                      className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full shadow-lg"
                    >
                      <i className="fas fa-plus mr-2"></i>
                      新規タスク
                    </button>
                  </div>
                  <table className="min-w-full divide-y divide-gray-200">
                    <thead className="bg-gray-50">
                      <tr>
                        <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                          番号
                        </th>
                        <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                          タイトル
                        </th>
                        <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                          ステータス
                        </th>
                        <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                          締め切り
                        </th>
                        <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                          編集
                        </th>
                        <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                          削除
                        </th>
                      </tr>
                    </thead>
                    <tbody className="bg-white divide-y divide-gray-200">
                      {tasks.map((task, index) => (
                        <tr key={task.id}>
                          <td className="px-6 py-4 whitespace-nowrap">
                            {index + 1}
                          </td>
                          <td className="px-6 py-4 whitespace-nowrap">
                            {task.title}
                          </td>
                          <td className="px-6 py-4 whitespace-nowrap">
                            {task._taskstatuses[0].name}
                          </td>
                          <td className="px-6 py-4 whitespace-nowrap">
                            {task.due_date}
                          </td>
                          <td className="px-6 py-4 whitespace-nowrap">
                            <i 
                              className="fas fa-edit text-blue-500 cursor-pointer"
                              onClick={() => handleEditClick(task.id)}
                            ></i>
                          </td>
                          <td className="px-6 py-4 whitespace-nowrap">
                            <i 
                              className="fas fa-trash-alt text-red-500 cursor-pointer"
                              onClick={() => handleDeleteClick(task.id)}
                            ></i>
                          </td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
              ) : (
                <p className="text-gray-500 mt-4">
                  ユーザー情報を読み込んでいます...
                </p>
              )}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

3.3.4. Task Creation Page

Task Creation Page はタスクの新規作成画面です。プロンプトではトークンを使用して実行するように指示しています。

newtask ページを作成します。
見出しは「タスク作成」とします。

以下のイメージを参考に、タスクの新規作成画面を作成します。
[ タスク登録ページのイメージ画像]


タスクの登録には以下のエンドポイントを使用します。
[Xano API のタスク登録用エンドポイントURL]

Bearerトークンは、ローカルストレージ authToken に保存されているトークンを使用します。

編集できる値は以下です。
user_id
title
description
taskstatuses_id
due_date

user_id は、以下のエンドポイントを使用して取得した id 
 を使用します。
[Xano API のユーザー情報用エンドポイントURL]


タスクステータスの値は、以下の固定値から選択することが出来ます。
1:未完了
2:進行中
3:完了

登録に成功したら、mytasks ページに遷移します。
失敗したら、エラーメッセージを表示します。

mytasks ページに遷移するための、戻るボタンを作成します。

生成されたコードは以下です。

function MainComponent() {
  const [title, setTitle] = React.useState('');
  const [description, setDescription] = React.useState('');
  const [status, setStatus] = React.useState('1');
  const [dueDate, setDueDate] = React.useState('');
  const [error, setError] = React.useState('');
  const [userId, setUserId] = React.useState(null);

  React.useEffect(() => {
    const fetchUserId = async () => {
      const authToken = localStorage.getItem('authToken');
      try {
        const response = await fetch('[Xano API のユーザー情報用エンドポイントURL]', {
          headers: {
            'Authorization': `Bearer ${authToken}`
          }
        });
        const data = await response.json();
        setUserId(data.id);
      } catch (error) {
        setError('ユーザー情報の取得に失敗しました');
      }
    };
    fetchUserId();
  }, []);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const authToken = localStorage.getItem('authToken');
    try {
      const response = await fetch('[Xano API のタスク登録用エンドポイントURL]', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${authToken}`
        },
        body: JSON.stringify({
          user_id: userId,
          title,
          description,
          taskstatuses_id: parseInt(status),
          due_date: dueDate
        })
      });
      if (response.ok) {
        window.location.href = '/mytasks';
      } else {
        setError('タスクの登録に失敗しました');
      }
    } catch (error) {
      setError('タスクの登録に失敗しました');
    }
  };

  return (
    <div className="min-h-screen bg-gray-100 py-6 flex flex-col justify-center sm:py-12">
      <div className="relative py-3 sm:max-w-xl sm:mx-auto">
        <div className="absolute inset-0 bg-gradient-to-r from-blue-300 to-blue-600 shadow-lg transform -skew-y-6 sm:skew-y-0 sm:-rotate-6 sm:rounded-3xl"></div>
        <div className="relative px-4 py-10 bg-white shadow-lg sm:rounded-3xl sm:p-20">
          <div className="max-w-md mx-auto">
            <h1 className="text-2xl font-semibold mb-6 text-center">タスク作成</h1>
            <form onSubmit={handleSubmit} className="space-y-4">
              <div>
                <label htmlFor="title" className="block text-sm font-medium text-gray-700">タイトル</label>
                <input type="text" id="title" name="title" required className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" value={title} onChange={(e) => setTitle(e.target.value)} />
              </div>
              <div>
                <label htmlFor="description" className="block text-sm font-medium text-gray-700">説明</label>
                <textarea id="description" name="description" rows="3" className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" value={description} onChange={(e) => setDescription(e.target.value)}></textarea>
              </div>
              <div>
                <label htmlFor="status" className="block text-sm font-medium text-gray-700">ステータス</label>
                <select id="status" name="status" className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" value={status} onChange={(e) => setStatus(e.target.value)}>
                  <option value="1">未完了</option>
                  <option value="2">進行中</option>
                  <option value="3">完了</option>
                </select>
              </div>
              <div>
                <label htmlFor="dueDate" className="block text-sm font-medium text-gray-700">期限</label>
                <input type="date" id="dueDate" name="dueDate" className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" value={dueDate} onChange={(e) => setDueDate(e.target.value)} />
              </div>
              {error && <p className="text-red-500 text-sm">{error}</p>}
              <div className="flex justify-between">
                <button type="button" onClick={() => window.location.href = '/mytasks'} className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded">戻る</button>
                <button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">登録</button>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
  );
}

3.3.5. Task Editor Page

Task Editor Page はタスク編集の画面です。

edittask ページを作成します。
見出しは「タスク編集」とします。
ページのデザインは、下図のデザインと同じものを作成します。
[ タスク編集ページのイメージ画像]

タスク情報の取得は以下のエンドポイントを使用します。
[Xano API のタスク情報用エンドポイントURL]

tasks_id には、クエリストリングの taskid で指定された値を使用します。Bearerトークンは、ローカルストレージ authToken に保存されているトークンを使用します。

以下の図は Tasksテーブルの定義です。
[XanoのTasksテーブル定義イメージ画像]


編集できる値は以下です。
title
description
taskstatuses_id
due_date

なおタスクステータスの値は、以下の固定値から選択することが出来ます。
1:未完了
2:進行中
3:完了

タスクの編集は、以下のエンドポイントをPATCHで実行します。
[Xano API のタスク編集用エンドポイントURL]

編集に成功したら、mytasks ページに遷移します。
失敗したら、エラーメッセージを表示します。

mytasks ページに遷移するための、戻るボタンを作成します。

生成されたコードは以下です。

function MainComponent() {
  const [task, setTask] = React.useState({
    title: '',
    description: '',
    taskstatuses_id: '',
    due_date: ''
  });
  const [errorMessage, setErrorMessage] = React.useState('');

  React.useEffect(() => {
    const fetchTask = async () => {
      const urlParams = new URLSearchParams(window.location.search);
      const taskId = urlParams.get('taskid');
      const authToken = localStorage.getItem('authToken');

      try {
        const response = await fetch(`[Xano API のタスク情報用エンドポイントURL]`, {
          headers: {
            'Authorization': `Bearer ${authToken}`
          }
        });
        if (!response.ok) throw new Error('Failed to fetch task');
        const data = await response.json();
        setTask(data);
      } catch (error) {
        setErrorMessage('タスクの取得に失敗しました');
      }
    };

    fetchTask();
  }, []);

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setTask(prevTask => ({ ...prevTask, [name]: value }));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    const authToken = localStorage.getItem('authToken');
    const urlParams = new URLSearchParams(window.location.search);
    const taskId = urlParams.get('taskid');

    try {
      const response = await fetch(`[Xano API のタスク編集用エンドポイントURL]`, {
        method: 'PATCH',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${authToken}`
        },
        body: JSON.stringify(task)
      });

      if (!response.ok) throw new Error('Failed to update task');
      window.location.href = '/mytasks';
    } catch (error) {
      setErrorMessage('タスクの更新に失敗しました');
    }
  };

  return (
    <div className="min-h-screen bg-gradient-to-br from-blue-100 to-white flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
      <div className="max-w-md w-full bg-white rounded-lg shadow-lg overflow-hidden">
        <div className="px-6 py-8">
          <h1 className="text-3xl font-bold mb-6 font-sans text-center text-gray-900">タスク編集</h1>
          
          <form onSubmit={handleSubmit} className="space-y-4">
            <div>
              <label htmlFor="title" className="block mb-1 font-medium font-sans text-gray-700">タイトル</label>
              <input type="text" id="title" name="title" value={task.title} onChange={handleInputChange} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
            </div>
            
            <div>
              <label htmlFor="description" className="block mb-1 font-medium font-sans text-gray-700">説明</label>
              <textarea id="description" name="description" value={task.description} onChange={handleInputChange} rows="4" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
            </div>
            
            <div>
              <label htmlFor="taskstatuses_id" className="block mb-1 font-medium font-sans text-gray-700">ステータス</label>
              <select id="taskstatuses_id" name="taskstatuses_id" value={task.taskstatuses_id} onChange={handleInputChange} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
                <option value="1">未完了</option>
                <option value="2">進行中</option>
                <option value="3">完了</option>
              </select>
            </div>
            
            <div>
              <label htmlFor="due_date" className="block mb-1 font-medium font-sans text-gray-700">期限</label>
              <input type="date" id="due_date" name="due_date" value={task.due_date} onChange={handleInputChange} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
            </div>
            
            <div className="flex justify-between">
              <button type="button" onClick={() => window.location.href = '/mytasks'} className="bg-gray-300 text-gray-700 px-6 py-2 rounded-md hover:bg-gray-400 font-sans transition duration-300 ease-in-out transform hover:-translate-y-1 hover:scale-105">
                戻る
              </button>
              <button type="submit" className="bg-blue-500 text-white px-6 py-2 rounded-md hover:bg-blue-600 font-sans transition duration-300 ease-in-out transform hover:-translate-y-1 hover:scale-105">
                登録
              </button>
            </div>
          </form>
          
          {errorMessage && <div className="mt-4 text-red-500 font-sans text-center">{errorMessage}</div>}
        </div>
      </div>
    </div>
  );
}

以上で Create のページ作成は完了です! View live して動作を確認してみてくださいね。

なお、Demo 機能ではページ遷移が上手く動作しない(404エラーが表示される)ため、動作確認は View live で行う必要があります。

プロンプトでは、ローカルストレージ(authToken)、クエリストリング(taskid)といったキー、URLで使用されるページ名(signup、mytasks、newtask、edittask)を具体的に指定しました。プロンプトで曖昧にしている場合は、想定していたような動作ではなく、コードを参照して処理内容を確認するといった作業が増えた為、動作に何かしらの影響を受けそうな部分に関しては、明確に指示することをおススメします。

なお、今回のサンプルでは、以前の記事でご紹介した Function 機能については使用しませんでした。理由としては、Function 内でローカルストレージを使ったテストが実行できなかった為(サーバーサイドでローカルストレージは使えない)ですが、ローカルストレージの使用をページ側の処理で行うなど工夫すれば Function 機能も利用できるかと思います。もし、エラーを受け取るなど、思ったようにアプリが生成されないといった場合は、処理がサーバーサイドとクライアントサイドのどちらで実行されているか?を考慮しておくと良さそうですね!

4. まとめ

今回は、Create.xyz と バックエンドサービスの Xano の連携についてご紹介しました!今回のアプリを作成する際、プロンプトでの指示については、実際に生成されたコードを確認しながら、プロンプトの内容を修正して進めるといった作業もありましたが、基本的には対話による生成で完結することができました。Create は進化も早く、新しい機能が日々追加されていくスピード感がありますね。ぜひ皆さんも、積極的に AI コーディングを試してみてくださいね!

では、次回もどうぞお楽しみに~!

5. 関連記事

以下は、Create.xyz に関する記事となっています。ぜひ、こちらも併せてご参考ください!

blog.nocodelab.jp

blog.nocodelab.jp

blog.nocodelab.jp

blog.nocodelab.jp