Next.js とSupabaseで求人マッチングアプリを作る③~企業のプロファイル変更~

前回に引き続き、求人マッチングアプリの制作方法をご紹介します!

先日作ったマスタデータに対して、UIを追加していきます。
今回は、以下のような『企業の基本情報を変更する』UIを追加しましょう。この記事をベースに、UI作成・DBとの紐づけを習得してみてください。

目次

Next.js実装

components/UserInfo.tsx

"use client";
import Link from "next/link";
import { createClient } from "@/utils/supabase/client";
import { useEffect, useState } from "react";
import { userType } from "@/utils/usertype";
import { Database } from "@/types/supabase";
import { userInfoContentsType } from "@/utils/userinfocontentstype";
import MainContents from "./userinfo/MainContents";

export default function UserInfo() {
  const [usertype, setUsertype] = useState<userType>(userType.none);
  const [userInfoContents, setUserInfoContents] =
    useState<userInfoContentsType>(userInfoContentsType.none);
  const supabase = createClient();

  const getUsertype = async () => {
    const { data, error } = await supabase.from("mst_user_type").select();
    if (data) {
      const data_usertype: Database["public"]["Tables"]["mst_user_type"]["Row"] =
        data[0];

      if (data_usertype.user_type !== process.env.NEXT_PUBLIC_USER_TYPE) {
        return;
      }
      switch (data_usertype.user_type) {
        case "job_seeker":
          setUsertype(userType.job_seeker);
          setUserInfoContents(userInfoContentsType.job_seeker_basic);
          break;
        case "company":
          setUsertype(userType.company);
          setUserInfoContents(userInfoContentsType.company_basic);
          break;
        case "admin":
          setUsertype(userType.admin);
          break;

        default:
          break;
      }
    }
  };

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

  return (
    <div className="animate-in flex-1 flex gap-20 opacity-0 max-w-4xl px-3">
      {usertype == userType.job_seeker ? (
        <>
          <aside
            id="default-sidebar"
            className="w-48 h-auto transition-transform -translate-x-full sm:translate-x-0"
            aria-label="Sidebar"
          >
            <div className="h-full px-3 py-4 overflow-y-auto bg-gray-50 dark:bg-gray-800">
              <ul className="space-y-2 font-medium">
                <li
                  className="text-center p-2 text-gray-900 rounded-lg hover:bg-gray-100"
                  onClick={() =>
                    setUserInfoContents(userInfoContentsType.job_seeker_basic)
                  }
                >
                  登録情報
                </li>
              </ul>
            </div>
          </aside>
          <main className="flex-1 flex flex-col gap-6 w-full">
            <MainContents contentsType={userInfoContents}></MainContents>
          </main>
        </>
      ) : usertype == userType.company ? (
        <>
          <aside
            id="default-sidebar"
            className="w-48 h-auto transition-transform -translate-x-full sm:translate-x-0"
            aria-label="Sidebar"
          >
            <div className="h-full px-3 py-4 overflow-y-auto bg-gray-50 dark:bg-gray-800">
              <ul className="space-y-2 font-medium">
                <li
                  className="text-center p-2 text-gray-900 rounded-lg hover:bg-gray-100"
                  onClick={() =>
                    setUserInfoContents(userInfoContentsType.company_basic)
                  }
                >
                  登録情報
                </li>
              </ul>
            </div>
          </aside>
          <main className="flex-1 flex flex-col gap-6 w-full">
            <MainContents contentsType={userInfoContents}></MainContents>
          </main>
        </>
      ) : usertype == userType.admin ? (
        <>管理者ページ</>
      ) : (
        <>
          <aside
            id="default-sidebar"
            className="w-48 h-auto transition-transform -translate-x-full sm:translate-x-0"
            aria-label="Sidebar"
          >
            <div className="h-full px-3 py-4 overflow-y-auto bg-gray-50 dark:bg-gray-800">
              <ul className="space-y-2 font-medium"></ul>
            </div>
          </aside>
          <main className="flex-1 flex flex-col gap-6 w-full">
            <MainContents contentsType={userInfoContents}></MainContents>
          </main>
        </>
      )}
    </div>
  );
}

こちらでユーザタイプとドメインが異なる場合の分岐のみ追加しています。

if (data_usertype.user_type !== process.env.NEXT_PUBLIC_USER_TYPE) {
   return;
}

components/userinfo/MainContents.tsx

以前作成した時点では、基本情報の変更画面が存在していないため追加します。
また`UserInfo.tsx`と同じく、ユーザタイプとドメインが異なる場合はエラーとして扱うための仕組みも入れています。

import { userInfoContentsType } from "@/utils/userinfocontentstype";
import JobSeekerBasic from "./JobSeekerBasic";
import CompanyBasic from "./CompanyBasic";

type Props = {
  contentsType: userInfoContentsType;
};

export default function MainContents({ contentsType }: Props) {
  return (
    <>
      {contentsType == userInfoContentsType.job_seeker_basic ? (
        <p>求職者ページ</p>
      ) : contentsType == userInfoContentsType.company_basic ? (
        <CompanyBasic></CompanyBasic>
      ) : (
        <p>
          ログインに問題があるようです。
          <br />
          求職者は求職者用のドメイン、企業は企業用のドメインからアクセスをお願いします。
        </p>
      )}
    </>
  );
}

components/userinfo/CompanyBasic.tsx

企業の基本情報を更新するための画面です。
内容としては`mst_company`のデータがあれば表示し、必須項目を入力したうえで更新を行えばSupabase側に反映されます。
コードは長いですが、そこまで難しいことは行っておりません。

"use client";
import { createClient } from "@/utils/supabase/client";
import { useState, useEffect } from "react";
import { redirect } from "next/navigation";
import { SubmitButton } from "../../components/SubmitButton";
import { Database } from "@/types/supabase";

export default function CompanyBasic() {
  const [message, setMessage] = useState("");
  const supabase = createClient();
  const [industryData, setIndustryData] = useState<
    Database["public"]["Tables"]["mst_industry"]["Row"][]
  >([]);

  // フォームデータを保持する
  const [companyname, setCompanyname] = useState("");
  const [zipcode, setZipcode] = useState("");
  const [address1, setAddress1] = useState("");
  const [address2, setAddress2] = useState("");
  const [phone, setPhone] = useState("");
  const [email1, setEmail1] = useState("");
  const [companyurl, setCompanyurl] = useState("");
  const [capital, setCapital] = useState<number>();
  const [employee, setEmployee] = useState<number>();
  const [annual_turnover, setAnnual_turnover] = useState<number>();
  const [established_at, setEstablishd_at] = useState("");
  const [industry_id_1, setIndustry_id_1] = useState<string | null>(null);
  const [industry_id_2, setIndustry_id_2] = useState<string | null>(null);
  const [industry_id_3, setIndustry_id_3] = useState<string | null>(null);
  const [company_image_url_1, setCompany_image_url_1] = useState("");
  const [company_image_url_2, setCompany_image_url_2] = useState("");
  const [company_image_url_3, setCompany_image_url_3] = useState("");
  const [company_image_url_4, setCompany_image_url_4] = useState("");
  const [company_image_url_5, setCompany_image_url_5] = useState("");

  useEffect(() => {
    getIndustyData();
    setData();
  }, []);

  const setData = async () => {
    const { data, error } = await supabase.from("mst_company").select();

    if (data != null && data?.length != 0) {
      const company_data: Database["public"]["Tables"]["mst_company"]["Row"] =
        data[0];
      setCompanyname(company_data.name);
      setZipcode(company_data.zipcode);
      setAddress1(company_data.address1);
      setAddress2(company_data.address2);
      if (company_data.phone != null) {
        setPhone(company_data.phone);
      }
      setEmail1(company_data.email1);
      if (company_data.url != null) {
        setCompanyurl(company_data.url);
      }
      if (company_data.capital != null) {
        setCapital(company_data.capital);
      }
      if (company_data.employee != null) {
        setEmployee(company_data.employee);
      }
      if (company_data.annual_turnover != null) {
        setAnnual_turnover(company_data.annual_turnover);
      }
      if (company_data.established_at != null) {
        setEstablishd_at(company_data.established_at);
      }
      if (company_data.industry_id_1 != null) {
        setIndustry_id_1(company_data.industry_id_1);
      }
      if (company_data.industry_id_2 != null) {
        setIndustry_id_2(company_data.industry_id_2);
      }
      if (company_data.industry_id_3 != null) {
        setIndustry_id_3(company_data.industry_id_3);
      }
      if (company_data.company_image_url_1 != null) {
        setCompany_image_url_1(company_data.company_image_url_1);
      }
      if (company_data.company_image_url_2 != null) {
        setCompany_image_url_2(company_data.company_image_url_2);
      }
      if (company_data.company_image_url_3 != null) {
        setCompany_image_url_3(company_data.company_image_url_3);
      }
      if (company_data.company_image_url_4 != null) {
        setCompany_image_url_4(company_data.company_image_url_4);
      }
      if (company_data.company_image_url_5 != null) {
        setCompany_image_url_5(company_data.company_image_url_5);
      }
    }
  };

  const getIndustyData = async () => {
    const { data, error } = await supabase.from("mst_industry").select();
    if (error) {
      console.log(error);
      return;
    }

    console.log(data);
    const fixed_data: Database["public"]["Tables"]["mst_industry"]["Row"][] =
      data;
    setIndustryData(fixed_data);
  };

  const onSubmit = async () => {
    const {
      data: { user },
    } = await supabase.auth.getUser();
    if (!user) {
      return redirect("/login");
    }
    const timeStamp = new Date().toISOString();

    const { error } = await supabase.from("mst_company").upsert({
      user_uid: user.id,
      name: companyname,
      zipcode: zipcode,
      address1: address1,
      address2: address2,
      phone: phone,
      email1: email1,
      url: companyurl,
      capital: capital,
      employee: employee,
      annual_turnover: annual_turnover,
      established_at: established_at,
      industry_id_1: industry_id_1,
      industry_id_2: industry_id_2,
      industry_id_3: industry_id_3,
      company_image_url_1: company_image_url_1,
      company_image_url_2: company_image_url_2,
      company_image_url_3: company_image_url_3,
      company_image_url_4: company_image_url_4,
      company_image_url_5: company_image_url_5,
      updated_at: timeStamp,
    });

    if (error) {
      console.log(error);

      setMessage("保存に失敗しました。");
      return;
    } else {
      setMessage("");
    }
    return redirect("/userpage");
  };

  return (
    <div className="flex-1 flex flex-col w-96 py-8 sm:max-w-md justify-center gap-2">
      <form className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground">
        <h2>企業の登録情報</h2>
        <label className="text-md" htmlFor="companyname">
          会社名*
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="companyname"
          placeholder="株式会社○○"
          value={companyname}
          onChange={(e) => setCompanyname(e.target.value)}
          required
        />
        <label className="text-md" htmlFor="zipcode">
          郵便番号*
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="zipcode"
          value={zipcode}
          onChange={(e) => setZipcode(e.target.value)}
          placeholder="0000000 ※ハイフンなしでご入力ください"
          required
        />
        <label className="text-md" htmlFor="address1">
          住所1*
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="address1"
          value={address1}
          onChange={(e) => setAddress1(e.target.value)}
          placeholder="東京都港区浜松町2丁目2番15号"
          required
        />
        <label className="text-md" htmlFor="address2">
          住所2*
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="address2"
          value={address2}
          onChange={(e) => setAddress2(e.target.value)}
          placeholder="浜松町ダイヤビル2F"
          required
        />
        <label className="text-md" htmlFor="phone">
          電話番号
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="phone"
          value={phone}
          onChange={(e) => setPhone(e.target.value)}
          placeholder="00000000000 ※ハイフンなしでご入力ください"
        />

        <label className="text-md" htmlFor="email1">
          メールアドレス*
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="email1"
          value={email1}
          onChange={(e) => setEmail1(e.target.value)}
          placeholder="email1@example.com"
          required
          type="email"
        />

        <label className="text-md" htmlFor="companyurl">
          ホームページURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="companyurl"
          placeholder="https://example.com"
          value={companyurl}
          onChange={(e) => setCompanyurl(e.target.value)}
        />

        <label className="text-md" htmlFor="capital">
          資本金
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="capital"
          value={capital}
          onChange={(e) => setCapital(parseInt(e.target.value))}
          placeholder="100 ※単位は万円"
          type="number"
        />
        <label className="text-md" htmlFor="employee">
          従業員数
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="employee"
          value={employee}
          onChange={(e) => setEmployee(parseInt(e.target.value))}
          placeholder="50"
          type="number"
        />

        <label className="text-md" htmlFor="annual_turnover">
          年商
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="annual_turnover"
          value={annual_turnover}
          onChange={(e) => setAnnual_turnover(parseInt(e.target.value))}
          placeholder="5000 ※単位は万円"
          type="number"
        />

        <label className="text-md" htmlFor="established_at">
          設立
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="established_at"
          value={established_at}
          onChange={(e) => setEstablishd_at(e.target.value)}
          type="date"
        />

        <label className="text-md" htmlFor="industry_id_1">
          業界1
        </label>
        <select
          name="industry_id_1"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={industry_id_1 ? industry_id_1 : ""}
          onChange={(e) => setIndustry_id_1(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {industryData.map((item) => (
            <option key={item.id} value={item.id}>
              {item.industry}
            </option>
          ))}
        </select>
        <label className="text-md" htmlFor="industry_id_2">
          業界2
        </label>
        <select
          name="industry_id_2"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={industry_id_2 ? industry_id_2 : ""}
          onChange={(e) => setIndustry_id_2(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {industryData.map((item) => (
            <option key={item.id} value={item.id}>
              {item.industry}
            </option>
          ))}
        </select>
        <label className="text-md" htmlFor="industry_id_3">
          業界3
        </label>
        <select
          name="industry_id_3"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={industry_id_3 ? industry_id_3 : ""}
          onChange={(e) => setIndustry_id_3(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {industryData.map((item) => (
            <option key={item.id} value={item.id}>
              {item.industry}
            </option>
          ))}
        </select>

        <label className="text-md" htmlFor="company_image_url_1">
          会社の画像1のURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="company_image_url_1"
          value={company_image_url_1}
          onChange={(e) => setCompany_image_url_1(e.target.value)}
          placeholder="会社の画像1のURL"
        />
        <label className="text-md" htmlFor="company_image_url_2">
          会社の画像2のURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="company_image_url_2"
          value={company_image_url_2}
          onChange={(e) => setCompany_image_url_2(e.target.value)}
          placeholder="会社の画像2のURL"
        />
        <label className="text-md" htmlFor="company_image_url_3">
          会社の画像3のURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="company_image_url_3"
          value={company_image_url_3}
          onChange={(e) => setCompany_image_url_3(e.target.value)}
          placeholder="会社の画像3のURL"
        />
        <label className="text-md" htmlFor="company_image_url_4">
          会社の画像4のURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="company_image_url_4"
          value={company_image_url_4}
          onChange={(e) => setCompany_image_url_4(e.target.value)}
          placeholder="会社の画像4のURL"
        />
        <label className="text-md" htmlFor="company_image_url_5">
          会社の画像5のURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="company_image_url_5"
          value={company_image_url_5}
          onChange={(e) => setCompany_image_url_5(e.target.value)}
          placeholder="会社の画像5のURL"
        />

        <SubmitButton
          formAction={onSubmit}
          className="bg-green-700 rounded-md px-4 py-2 text-foreground mb-2"
          pendingText="企業情報更新中..."
        >
          保存
        </SubmitButton>
        {message !== "" && (
          <p className="mt-4 p-4 bg-foreground/10 text-foreground text-center">
            {message}
          </p>
        )}
      </form>
    </div>
  );
}

これで今回の実装はできたため、確認してみましょう!

実装の確認

masterにプッシュしてvercelのデプロイ完了を待ちます。
デプロイが終わったら、企業側のドメインで作成したユーザでログインします。

メールアドレスをクリックし、ユーザページにアクセスすると前回と違い、企業の登録情報画面がまず表示されます。

必須項目を入力して情報を更新すると、保存が完了し、Supabase側でも行が追加されたことを確認できます。

プロファイルの変更を確認できました!

その他参考資料など

また弊社では『マッチングワン』という『低コスト・短期にマッチングサービスを構築できる』サービスを展開しており、今回ご紹介するコードは、その『マッチングワン』でも使われているコードとなります。
本記事で紹介したようなプロダクトを開発されたい場合は、是非お問い合わせください。

またTodoONada株式会社では、この記事で紹介した以外にも、Supabase・Next.jsを使ったアプリの作成方法についてご紹介しています。
下記記事にて一覧を見ることができますので、ぜひこちらもご覧ください!https://note.com/embed/notes/n522396165049

お問合せ&各種リンク

presented by

  • システム開発、アプリ開発
  • マッチングアプリ開発
  • インフラ構築支援等、なんでもご相談ください。
よかったらシェアしてね!
  • URLをコピーしました!
目次