前回に引き続き、求人マッチングアプリの制作方法をご紹介します!
先日作ったマスタデータに対して、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
お問合せ&各種リンク
- お問合せ:GoogleForm
- ホームページ:https://libproc.com
- 運営会社:TodoONada株式会社
- Twitter:https://twitter.com/Todoonada_corp
- Instagram:https://www.instagram.com/todoonada_corp/
- Youtube:https://www.youtube.com/@todoonada_corp/
- Tiktok:https://www.tiktok.com/@todoonada_corp
presented by