今回から一覧系ページ、詳細系ページの作成を進めます!
具体的には
- 企業一覧
- 企業詳細
- 求職者一覧
- 求職者詳細
- 求人一覧
- 求人詳細
になります。
今回は企業一覧と企業詳細を作成します。
単に情報取得・表示だけでなく、ポリシーの調整も含まれているため、そのあたりも意識して制作していきましょう。
Supabase側の設定
今まで企業テーブル(mst_company)は自社のアカウントでした取得できないようにしていましたが、
今回求職者が企業一覧を見ることができるようにしたいため、`SELECT`ポリシーをtrueに変更します。
(合わせてNext.js側の実装タイミングで企業情報編集画面のデータ取得をid指定で行う仕組みに変更します。)
mst_companyのポリシー変更
Supabaseダッシュボード→`Table Editor`から`mst_company`を右クリックしてポリシー編集画面を開きます。
添付画像のようにtrueのみに変更します。
Next.jsの実装
components/userinfo/CompanyBasic.tsx
企業情報の編集画面内ですが、先ほどのポリシー変更の影響を受けているためそこだけ修正します。
"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<string | null>(null);
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: { user },
} = await supabase.auth.getUser();
if (!user) {
return redirect("/login");
}
const { data, error } = await supabase
.from("mst_company")
.select()
.eq("user_uid", user.id);
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;
}
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 ? 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>
);
}
`setData`関数内の下記の部分が変更されています。
useridで絞ってデータ取得をする仕組みに変更しています。
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
return redirect("/login");
}
const { data, error } = await supabase
.from("mst_company")
.select()
.eq("user_uid", user.id);
app/companylist/page.tsx
企業一覧ページです。
求職者側でログインしたときだけ確認できるようリダイレクトの仕組みを作成しています。
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
import { getUserTypeFromEnv, userType } from "@/utils/usertype";
import CompanyList from "@/components/jobseeker/CompanyLIst";
export default async function CompanyListPage() {
const supabase = createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
return redirect("/login");
}
const usertype = getUserTypeFromEnv();
if (usertype !== userType.job_seeker) {
return redirect("/");
}
return (
<div className="flex-1 w-full flex flex-col items-center">
<Header />
<CompanyList></CompanyList>
<Footer />
</div>
);
}
app/companydetail/page.tsx
企業の詳細情報ページです。
企業IDがurlパラメータに渡されていると、その企業の情報を子コンポーネントで取得できます。
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
import { getUserTypeFromEnv, userType } from "@/utils/usertype";
import JobDetail from "@/components/jobseeker/JobDetail";
export default async function JobDetailPage({
searchParams,
}: {
searchParams: { jobid: string };
}) {
const supabase = createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
return redirect("/login");
}
const usertype = getUserTypeFromEnv();
if (usertype !== userType.job_seeker) {
return redirect("/");
}
return (
<div className="flex-1 w-full flex flex-col items-center">
<Header />
<JobDetail jobid={searchParams.jobid}></JobDetail>
<Footer />
</div>
);
}
components/jobseeker/CompanyList.tsx
企業一覧を表示するコンポーネントです。
取得した企業情報をすべて一覧で表示します。
"use client";
import { createClient } from "@/utils/supabase/client";
import { useState, useEffect } from "react";
import { Database } from "@/types/supabase";
import Link from "next/link";
export default function CompanyList() {
const supabase = createClient();
const [companyData, setCompanyData] = useState<
Database["public"]["Tables"]["mst_company"]["Row"][]
>([]);
useEffect(() => {
getCompanyData();
}, []);
const getCompanyData = async () => {
const { data, error } = await supabase.from("mst_company").select();
if (error) {
console.log(error);
return;
}
console.log(data);
const tmp_data: Database["public"]["Tables"]["mst_company"]["Row"][] =
data as Database["public"]["Tables"]["mst_company"]["Row"][];
setCompanyData(tmp_data);
};
return (
<div className="flex-1 flex flex-col w-full py-8 sm:max-w-md justify-center gap-2 animate-in">
<h1>企業一覧</h1>
<ul>
{companyData.map((item) => (
<li key={item.user_uid}>
<Link
href={"/companydetail?companyid=" + item.user_uid}
className="text-blue-700 hover:text-blue-600 border-b"
>
{item.name}
</Link>
</li>
))}
</ul>
</div>
);
}
components/jobseeker/CompanyDetail.tsx
企業一覧からリンクを通じて遷移してくる詳細ページです。
現在画像アップロードの仕組みはないため、画像URLの表示はスキップしています。
"use client";
import { createClient } from "@/utils/supabase/client";
import { useState, useEffect } from "react";
import { Database } from "@/types/supabase";
import Link from "next/link";
type Props = {
companyid: string | null;
};
export default function CompanyDetail({ companyid }: Props) {
const supabase = createClient();
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<string | null>(null);
const [industry_name_1, setIndustry_name_1] = useState<string | null>(null);
const [industry_name_2, setIndustry_name_2] = useState<string | null>(null);
const [industry_name_3, setIndustry_name_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(() => {
setData();
}, []);
const setData = async () => {
if (companyid === "") {
return;
}
const { data, error } = await supabase
.from("mst_company")
.select()
.eq("user_uid", companyid);
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) {
const name = await getIndustyName(company_data.industry_id_1);
setIndustry_name_1(name);
}
if (company_data.industry_id_2 != null) {
const name = await getIndustyName(company_data.industry_id_2);
setIndustry_name_2(name);
}
if (company_data.industry_id_3 != null) {
const name = await getIndustyName(company_data.industry_id_3);
setIndustry_name_3(name);
}
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 getIndustyName = async (id: string) => {
const { data, error } = await supabase
.from("mst_industry")
.select()
.eq("id", id);
if (error) {
console.log(error);
return;
}
return data[0].industry;
};
return (
<div className="flex-1 flex flex-col w-full py-8 sm:max-w-md justify-center gap-2 animate-in">
<h1>企業詳細</h1>
<ul>
<li>
<dl className="flex">
<dt className="p-3 w-48 bg-gray-200">会社名</dt>
<dd className="p-3">{companyname}</dd>
</dl>
</li>
<li className="mt-2">
<dl className="flex">
<dt className="p-3 w-48 bg-gray-200">郵便番号</dt>
<dd className="p-3">{zipcode}</dd>
</dl>
</li>
<li className="mt-2">
<dl className="flex">
<dt className="p-3 w-48 bg-gray-200">住所1</dt>
<dd className="p-3">{address1}</dd>
</dl>
</li>
<li className="mt-2">
<dl className="flex">
<dt className="p-3 w-48 bg-gray-200">住所2</dt>
<dd className="p-3">{address2}</dd>
</dl>
</li>
<li className="mt-2">
<dl className="flex">
<dt className="p-3 w-48 bg-gray-200">電話番号</dt>
<dd className="p-3">{phone}</dd>
</dl>
</li>
<li className="mt-2">
<dl className="flex">
<dt className="p-3 w-48 bg-gray-200">メール</dt>
<dd className="p-3">{email1}</dd>
</dl>
</li>
<li className="mt-2">
<dl className="flex">
<dt className="p-3 w-48 bg-gray-200">企業URL</dt>
<dd className="p-3">{companyurl}</dd>
</dl>
</li>
<li className="mt-2">
<dl className="flex">
<dt className="p-3 w-48 bg-gray-200">資本金</dt>
<dd className="p-3">{capital}</dd>
</dl>
</li>
<li className="mt-2">
<dl className="flex">
<dt className="p-3 w-48 bg-gray-200">従業員数</dt>
<dd className="p-3">{employee}</dd>
</dl>
</li>
<li className="mt-2">
<dl className="flex">
<dt className="p-3 w-48 bg-gray-200">年商</dt>
<dd className="p-3">{annual_turnover}</dd>
</dl>
</li>
<li className="mt-2">
<dl className="flex">
<dt className="p-3 w-48 bg-gray-200">設立</dt>
<dd className="p-3">{established_at}</dd>
</dl>
</li>
<li className="mt-2">
<dl className="flex">
<dt className="p-3 w-48 bg-gray-200">業界1</dt>
<dd className="p-3">{industry_name_1}</dd>
</dl>
</li>
<li className="mt-2">
<dl className="flex">
<dt className="p-3 w-48 bg-gray-200">業界2</dt>
<dd className="p-3">{industry_name_2}</dd>
</dl>
</li>
<li className="mt-2">
<dl className="flex">
<dt className="p-3 w-48 bg-gray-200">業界3</dt>
<dd className="p-3">{industry_name_3}</dd>
</dl>
</li>
</ul>
</div>
);
}
components/Header.tsx
ヘッダーに一覧ページ表示に必要なリンクを追加しています。
import { createClient } from "@/utils/supabase/server";
import Link from "next/link";
import { redirect } from "next/navigation";
import { userType, getUserTypeFromEnv } from "@/utils/usertype";
export default async function Header() {
const supabase = createClient();
const usertype = getUserTypeFromEnv();
const {
data: { user },
} = await supabase.auth.getUser();
const signOut = async () => {
"use server";
const supabase = createClient();
await supabase.auth.signOut();
return redirect("/login");
};
return (
<nav className="w-full flex justify-center border-b border-b-foreground/10 h-16">
<div className="w-full max-w-4xl flex justify-between items-center p-3 text-sm">
{user && usertype == userType.company ? (
<div className="flex items-center gap-4">
<Link href="/myjoblist">自社の求人一覧</Link>
<Link href="/jobseekerlist">求職者一覧</Link>
<Link href="/userpage">ユーザページ</Link>
<form action={signOut}>
<button className="py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover">
ログアウト
</button>
</form>
</div>
) : user && usertype == userType.job_seeker ? (
<div className="flex items-center gap-4">
<Link href="/joblist">求人一覧</Link>
<Link href="/companylist">企業一覧</Link>
<Link href="/userpage">ユーザページ</Link>
<form action={signOut}>
<button className="py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover">
ログアウト
</button>
</form>
</div>
) : (
<Link
href="/login"
className="py-2 px-3 flex rounded-md no-underline bg-btn-background hover:bg-btn-background-hover"
>
ログイン
</Link>
)}
</div>
</nav>
);
}
それでは実装を確認してみます。
実装の確認
求職者側のアプリにアクセスし、ログインしましょう。
企業一覧へのリンクがヘッダーに表示されることがわかります。
これをクリックすると、企業の一覧ページにアクセスできます。
企業側のアプリで作成した企業情報の一覧が表示されます。
(現在一つしか入れていないので一つしか表示されませんが、複数追加すれば複数表示されます。)
こちらの企業リンクをクリックすると企業の詳細情報が表示されます。
これで企業の一覧情報と詳細情報にアクセスすることができるようになりました。
その他参考資料など
弊社では『マッチングワン』という『低コスト・短期にマッチングサービスを構築できる』サービスを展開しており、今回ご紹介するコードは、その『マッチングワン』でも使われているコードとなります。
本記事で紹介したようなプロダクトを開発されたい場合は、是非お問い合わせください。
また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