今回は前回の記事の続きで、『求職者の基本情報を変更する』UIを追加します。
基本的な流れは『企業のプロファイル変更』と同じです。
なのでもし自学目的で見ておられる方は、企業プロファイルのコードを見ながら、求職者部分のUI実装を行ってみてください。
Next.js実装
components/userinfo/MainContents.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 ? (
<JobSeekerBasic></JobSeekerBasic>
) : contentsType == userInfoContentsType.company_basic ? (
<CompanyBasic></CompanyBasic>
) : (
<p>
ログインに問題があるようです。
<br />
求職者は求職者用のドメイン、企業は企業用のドメインからアクセスをお願いします。
</p>
)}
</>
);
}
components/userinfo/JobSeekerBasic.tsx
求職者の基本情報を更新するための画面です。
内容としては`mst_job_seeker`のデータがあれば表示し、必須項目を入力したうえで更新を行えば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 JobSeekerBasic() {
const [message, setMessage] = useState("");
const supabase = createClient();
const [nationalityData, setNationalityData] = useState<
Database["public"]["Tables"]["mst_nationality"]["Row"][]
>([]);
const [occupationData, setOccupationData] = useState<
Database["public"]["Tables"]["mst_occupation"]["Row"][]
>([]);
const [residenceQualificationData, setResidenceQualificationData] = useState<
Database["public"]["Tables"]["mst_residence_qualification"]["Row"][]
>([]);
// フォームデータを保持する
const [lastname, setLastname] = useState("");
const [firstname, setFirstname] = useState("");
const [middlename, setMiddlename] = useState("");
const [gender, setGender] = useState("");
const [birthday, setBirthday] = useState("");
const [zipcode, setZipcode] = useState("");
const [address1, setAddress1] = useState("");
const [address2, setAddress2] = useState("");
const [phone, setPhone] = useState("");
const [email1, setEmail1] = useState("");
const [email2, setEmail2] = useState("");
const [email3, setEmail3] = useState("");
const [nationality_id, setNationality_id] = useState<string | null>(null);
const [current_annual_income, setCurrent_annual_income] = useState<number>();
const [desired_annual_income, setDesired_annual_income] = useState<number>();
const [spouse, setSpouse] = useState<string | null>(null);
const [desired_occupation_id_1, setDesired_occupation_id_1] = useState<
string | null
>(null);
const [desired_occupation_id_2, setDesired_occupation_id_2] = useState<
string | null
>(null);
const [desired_occupation_id_3, setDesired_occupation_id_3] = useState<
string | null
>(null);
const [desired_change_job_date, setDesired_change_job_date] = useState<
string | null
>(null);
const [residence_qualification_id, setResidence_qualification_id] = useState<
string | null
>(null);
const [residence_qualification_expired, setResidence_qualification_expired] =
useState<string | null>(null);
const [
residence_qualification_front_image_url,
setResidence_qualification_front_image_url,
] = useState("");
const [
residence_qualification_back_image_url,
setResidence_qualification_back_image_url,
] = useState("");
const [profile_image_url, setProfile_image_url] = useState("");
const [resume_file_url, setResume_file_url] = useState("");
const [resume_file_name, setResume_file_name] = useState("");
useEffect(() => {
getNationalityData();
getOccupationData();
getResidenceQualificationData();
setData();
}, []);
const setData = async () => {
const { data, error } = await supabase.from("mst_job_seeker").select();
if (data != null && data?.length != 0) {
const job_seeker_data: Database["public"]["Tables"]["mst_job_seeker"]["Row"] =
data[0];
setLastname(job_seeker_data.last_name);
setFirstname(job_seeker_data.first_name);
if (job_seeker_data.middle_name != null) {
setMiddlename(job_seeker_data.middle_name);
}
setGender("" + job_seeker_data.gender);
setBirthday(job_seeker_data.birthday);
if (job_seeker_data.zipcode != null) {
setZipcode(job_seeker_data.zipcode);
}
if (job_seeker_data.address1 != null) {
setAddress1(job_seeker_data.address1);
}
if (job_seeker_data.address2 != null) {
setAddress2(job_seeker_data.address2);
}
if (job_seeker_data.phone != null) {
setPhone(job_seeker_data.phone);
}
setEmail1(job_seeker_data.email1);
if (job_seeker_data.email2 != null) {
setEmail2(job_seeker_data.email2);
}
if (job_seeker_data.email3 != null) {
setEmail3(job_seeker_data.email3);
}
if (job_seeker_data.nationality_id != null) {
setNationality_id(job_seeker_data.nationality_id);
}
if (job_seeker_data.current_annual_income != null) {
setCurrent_annual_income(job_seeker_data.current_annual_income);
}
if (job_seeker_data.desired_annual_income != null) {
setDesired_annual_income(job_seeker_data.desired_annual_income);
}
if (job_seeker_data.spouse != null) {
setSpouse("" + job_seeker_data.spouse);
}
if (job_seeker_data.desired_occupation_id_1 != null) {
setDesired_occupation_id_1(job_seeker_data.desired_occupation_id_1);
}
if (job_seeker_data.desired_occupation_id_2 != null) {
setDesired_occupation_id_2(job_seeker_data.desired_occupation_id_2);
}
if (job_seeker_data.desired_occupation_id_3 != null) {
setDesired_occupation_id_3(job_seeker_data.desired_occupation_id_3);
}
if (job_seeker_data.desired_change_job_date != null) {
setDesired_change_job_date(job_seeker_data.desired_change_job_date);
}
if (job_seeker_data.residence_qualification_id != null) {
setResidence_qualification_id(
job_seeker_data.residence_qualification_id
);
}
if (job_seeker_data["residence qualification_expired"] != null) {
setResidence_qualification_expired(
job_seeker_data["residence qualification_expired"]
);
}
if (job_seeker_data["residence qualification_front_image_url"] != null) {
setResidence_qualification_front_image_url(
job_seeker_data["residence qualification_front_image_url"]
);
}
if (job_seeker_data["residence qualification_back_image_url"] != null) {
setResidence_qualification_back_image_url(
job_seeker_data["residence qualification_back_image_url"]
);
}
if (job_seeker_data.profile_image_url != null) {
setProfile_image_url(job_seeker_data.profile_image_url);
}
if (job_seeker_data.resume_file_url != null) {
setResume_file_url(job_seeker_data.resume_file_url);
}
if (job_seeker_data.resume_file_name != null) {
setResume_file_name(job_seeker_data.resume_file_name);
}
}
};
const getNationalityData = async () => {
const { data, error } = await supabase.from("mst_nationality").select();
if (error) {
console.log(error);
return;
}
const fixed_data: Database["public"]["Tables"]["mst_nationality"]["Row"][] =
data;
setNationalityData(fixed_data);
};
const getOccupationData = async () => {
const { data, error } = await supabase.from("mst_occupation").select();
if (error) {
console.log(error);
return;
}
const fixed_data: Database["public"]["Tables"]["mst_occupation"]["Row"][] =
data;
setOccupationData(fixed_data);
};
const getResidenceQualificationData = async () => {
const { data, error } = await supabase
.from("mst_residence_qualification")
.select();
if (error) {
console.log(error);
return;
}
const fixed_data: Database["public"]["Tables"]["mst_residence_qualification"]["Row"][] =
data;
setResidenceQualificationData(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_job_seeker").upsert({
user_uid: user.id,
last_name: lastname,
first_name: firstname,
middle_name: middlename,
gender: parseInt(gender),
birthday: birthday,
zipcode: zipcode,
address1: address1,
address2: address2,
phone: phone,
email1: email1,
email2: email2,
email3: email3,
nationality_id: nationality_id,
current_annual_income: current_annual_income,
desired_annual_income: desired_annual_income,
spouse: spouse,
desired_occupation_id_1: desired_occupation_id_1,
desired_occupation_id_2: desired_occupation_id_2,
desired_occupation_id_3: desired_occupation_id_3,
desired_change_job_date: desired_change_job_date,
residence_qualification_id: residence_qualification_id,
"residence qualification_expired": residence_qualification_expired,
"residence qualification_front_image_url":
residence_qualification_front_image_url,
"residence qualification_back_image_url":
residence_qualification_back_image_url,
profile_image_url: profile_image_url,
resume_file_url: resume_file_url,
resume_file_name: resume_file_name,
updated_at: timeStamp,
});
console.log(error);
if (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="lastname">
姓*
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="lastname"
placeholder="山田"
value={lastname}
onChange={(e) => setLastname(e.target.value)}
required
/>
<label className="text-md" htmlFor="firstname">
名*
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="firstname"
placeholder="太郎"
value={firstname}
onChange={(e) => setFirstname(e.target.value)}
required
/>
<label className="text-md" htmlFor="middlename">
ミドルネーム
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="middlename"
value={middlename}
onChange={(e) => setMiddlename(e.target.value)}
placeholder="ミドルネーム"
/>
<label className="text-md" htmlFor="gender">
性別*
</label>
<select
name="gender"
className="rounded-md px-4 py-2 bg-inherit border mb-6"
value={gender}
onChange={(e) => setGender(e.target.value)}
required
>
<option value="">--選択してください--</option>
<option value="1">男性</option>
<option value="0">女性</option>
<option value="2">その他</option>
</select>
<label className="text-md" htmlFor="birthday">
誕生日*
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="birthday"
value={birthday}
onChange={(e) => setBirthday(e.target.value)}
type="date"
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 ※ハイフンなしでご入力ください"
/>
<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号"
/>
<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"
/>
<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">
メールアドレス1*
</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"
type="email"
required
/>
<label className="text-md" htmlFor="email2">
メールアドレス2
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="email2"
value={email2}
onChange={(e) => setEmail2(e.target.value)}
placeholder="email2@example.com"
type="email"
/>
<label className="text-md" htmlFor="email3">
メールアドレス3
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="email3"
value={email3}
onChange={(e) => setEmail3(e.target.value)}
placeholder="email3@example.com"
type="email"
/>
<label className="text-md" htmlFor="nationalityId">
国籍
</label>
<select
name="nationalityId"
className="rounded-md px-4 py-2 bg-inherit border mb-6"
value={nationality_id ? nationality_id : ""}
onChange={(e) => setNationality_id(e.target.value)}
>
<option value="">--選択してください--</option>
{nationalityData.map((item) => (
<option key={item.id} value={item.id}>
{item.nationality}
</option>
))}
</select>
<label className="text-md" htmlFor="current_annual_income">
現在の年収
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="current_annual_income"
value={current_annual_income}
onChange={(e) => setCurrent_annual_income(parseInt(e.target.value))}
placeholder="現在の年収"
type="number"
/>
<label className="text-md" htmlFor="desired_annual_income">
希望年収
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="desired_annual_income"
value={desired_annual_income}
onChange={(e) => setDesired_annual_income(parseInt(e.target.value))}
placeholder="希望年収"
type="number"
/>
<label className="text-md" htmlFor="spouse">
配偶者
</label>
<select
name="spouse"
className="rounded-md px-4 py-2 bg-inherit border mb-6"
value={spouse ? spouse : ""}
onChange={(e) => setSpouse(e.target.value)}
>
<option value="">--選択してください--</option>
<option value="false">なし</option>
<option value="true">あり</option>
</select>
<label className="text-md" htmlFor="desired_occupation_id_1">
希望職種1
</label>
<select
name="desired_occupation_id_1"
className="rounded-md px-4 py-2 bg-inherit border mb-6"
value={desired_occupation_id_1 ? desired_occupation_id_1 : ""}
onChange={(e) => setDesired_occupation_id_1(e.target.value)}
>
<option value="">--選択してください--</option>
{occupationData.map((item) => (
<option key={item.id} value={item.id}>
{item.occupation}
</option>
))}
</select>
<label className="text-md" htmlFor="desired_occupation_id_2">
希望職種2
</label>
<select
name="desired_occupation_id_2"
className="rounded-md px-4 py-2 bg-inherit border mb-6"
value={desired_occupation_id_2 ? desired_occupation_id_2 : ""}
onChange={(e) => setDesired_occupation_id_2(e.target.value)}
>
<option value="">--選択してください--</option>
{occupationData.map((item) => (
<option key={item.id} value={item.id}>
{item.occupation}
</option>
))}
</select>
<label className="text-md" htmlFor="desired_occupation_id_3">
希望職種3
</label>
<select
name="desired_occupation_id_3"
className="rounded-md px-4 py-2 bg-inherit border mb-6"
value={desired_occupation_id_3 ? desired_occupation_id_3 : ""}
onChange={(e) => setDesired_occupation_id_3(e.target.value)}
>
<option value="">--選択してください--</option>
{occupationData.map((item) => (
<option key={item.id} value={item.id}>
{item.occupation}
</option>
))}
</select>
<label className="text-md" htmlFor="desired_change_job_date">
転職希望日
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="desired_change_job_date"
value={desired_change_job_date ? desired_change_job_date : ""}
onChange={(e) => setDesired_change_job_date(e.target.value)}
type="date"
/>
<label className="text-md" htmlFor="residence_qualification_id">
在留資格
</label>
<select
name="residence_qualification_id"
className="rounded-md px-4 py-2 bg-inherit border mb-6"
value={residence_qualification_id ? residence_qualification_id : ""}
onChange={(e) => setResidence_qualification_id(e.target.value)}
>
<option value="">--選択してください--</option>
{residenceQualificationData.map((item) => (
<option key={item.id} value={item.id}>
{item.residence_qualification}
</option>
))}
</select>
<label className="text-md" htmlFor="residence_qualification_expired">
在留資格期限
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="residence_qualification_expired"
value={
residence_qualification_expired
? residence_qualification_expired
: ""
}
onChange={(e) => setResidence_qualification_expired(e.target.value)}
type="date"
/>
<label
className="text-md"
htmlFor="residence_qualification_front_image_url"
>
在留資格カード表のファイルURL
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="residence_qualification_front_image_url"
value={residence_qualification_front_image_url}
onChange={(e) =>
setResidence_qualification_front_image_url(e.target.value)
}
placeholder="在留資格カード表のファイルURL"
/>
<label
className="text-md"
htmlFor="residence_qualification_back_image_url"
>
在留資格カード裏のファイルURL
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="residence_qualification_back_image_url"
value={residence_qualification_back_image_url}
onChange={(e) =>
setResidence_qualification_back_image_url(e.target.value)
}
placeholder="在留資格カード裏のファイルURL"
/>
<label className="text-md" htmlFor="profile_image_url">
プロフィール画像のファイルURL
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="profile_image_url"
value={profile_image_url}
onChange={(e) => setProfile_image_url(e.target.value)}
placeholder="プロフィール画像のファイルURL"
/>
<label className="text-md" htmlFor="resume_file_url">
履歴書のファイルURL
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="resume_file_url"
value={resume_file_url}
onChange={(e) => setResume_file_url(e.target.value)}
placeholder="履歴書のファイルURL"
/>
<label className="text-md" htmlFor="resume_file_name">
履歴書のファイル名
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="resume_file_name"
value={resume_file_name}
onChange={(e) => setResume_file_name(e.target.value)}
placeholder="履歴書のファイル名"
/>
<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