🙆‍♀️

WEBサイト制作(Next.js,Prisma,Supabase,ReactEmail)

に公開

はじめに

著者は八丈島という離島に住み、趣味でプログラムをしています。
はじめてお仕事としてサイト制作を依頼されたため、その内容をまとめたものになります。
なお、不動産の実際の情報はまだ掲載されていません。

サイト概要

新しく不動産事業を始めたいという知人より、某不動産サイトを模したサイトを作成してほしいとのことでしたので色合いなど似ているところがあります。

URL

https://www.8jo-real-estate.jp/

GitHub

https://github.com/kiyo-8jo/real-estate

主な使用技術

・フロントエンド,バックエンド
 Next.js
 TypeScript

・スライダー
 Swiper

・ORM
 Prisma

・DB,ストレージ
 Supabase

・デプロイ
 Vercel

・メール
 react-email
Resend

・地図
 react-google-maps/api

バージョン

package.json
・・・
"dependencies": {
    "@prisma/client": "^6.5.0",
    "@react-email/components": "^0.0.36",
    "@react-google-maps/api": "^2.20.6",
    "@supabase/ssr": "^0.6.1",
    "@supabase/supabase-js": "^2.49.4",
    "next": "^15.0.7",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-icons": "^5.5.0",
    "resend": "^4.2.0",
    "swiper": "^11.2.6"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "15.0.3",
    "prisma": "^6.5.0",
    "react-email": "^4.0.7",
    "typescript": "^5"
  }
・・・

ER図

ページ・機能解説

実際のページは上記URLからご確認ください。

ホームページ

・地図(画像)
画像をクリックすることで対象の地域、賃貸または購入ページに遷移します。

・新着情報
新着順に5件の物件情報を掲載しています。
文字情報はSupabaseのDBより、画像はSupabaseStorageより引っ張ってきています。
クリックすることで詳細ページに遷移します。

賃貸ページ,購入ページ

・地図(GoogleMap)
react-google-maps/apiを使用しています。物件名をクリックすることでその物件の詳細ページに遷移します。

・フィルター用ボタン
地域と物件の種類で、物件にフィルターを掛けることができます。また、リセットボタンを押すことでフィルターを解除できます。

・ソート用プルダウンメニュー
価格順、おすすめ順、新着順、築年数順、面積順で物件をソートすることができます。デフォルトはおすすめ順です。

・物件情報
クリックすることで詳細ページに遷移します。

詳細ページ

・問い合わせボタン
クリックすることでその物件の問い合わせページに遷移します。

・スライダー
sliderを使用しています。SupabaseStorageから画像を引っ張っています。

・地図(GoogleMap)
その物件の座標に合わせてGoogleMapが表示されます。

問い合わせページ

オーソドックスなフォームです。必要な情報に入力がなければメール送信はできません。送信に成功した際はモーダルで通知します。メール送信にはreact-emailとResendを使用しています。

オーナーページ

問い合わせページと同様です。

ページごとのレンダリング状況

Route (app)
┌ ƒ /
├ ○ /_not-found
├ ƒ /api/getAllBuyData
├ ƒ /api/getAllData
├ ƒ /api/getAllRentData
├ ƒ /api/getBuyData
├ ƒ /api/getDetailData/[id]
├ ƒ /api/getRentData
├ ƒ /api/sendEmail/detailContact
├ ƒ /api/sendEmail/ownerContact
├ ƒ /buy
├ ○ /contact
├ ○ /detail/[id]
├ ○ /detail/[id]/contact
├ ○ /owner
└ ƒ /rent

○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand

メール文面

賃貸、購入の場合


どの物件の詳細ページからメールを送信しているかわかるようにしているので、メールにもどの物件についてのメールなのかわかるようにしています。

オーナーの場合


賃貸、購入の場合同様にオーナーからのメールだとわかるようにしています。

こだわった部分

PhotoShopを使って画像作成


ホームページの画像はPhotoShopを使って自作しました。
一から勉強して作成したものなので思い入れがあります。

フィルター、ソート機能


クエリパラメータを使用しました。

FilterAndResetButtons.tsx
"use client";

import { useState } from "react";
import AreaFilterButtons from "./areaFilterButtons/AreaFilterButtons";
import AreaFilterResetButton from "./areaFilterResetButton/AreaFilterResetButton";
import styles from "./FilterAndResetButtons.module.css";
import BuildingTypeFilterResetButton from "./buildingTypeFilterResetButton/BuildingTypeFilterResetButton";
import BuildingFilterButtons from "./buildingFilterButtons/BuildingFilterButtons";
import Select from "./select/Select";
import { usePathname, useSearchParams } from "next/navigation";

const FilterAndResetButtons = () => {
  const searchParams = useSearchParams();
  // rentPageかbuyPageか判別
  const type = usePathname();

  const [area, setArea] = useState<string | null>(
    searchParams.get("area") || null
  );
  const [buildingType, setBuildingType] = useState<string | null>(
    searchParams.get("buildingType")
  );
  const [sort, setSort] = useState<string>("recommendation");

  return (
    <div className={styles.wrapper}>
      <div className={styles.button_container}>
        <p>地域</p>
        <AreaFilterButtons
          area={area}
          setArea={setArea}
          buildingType={buildingType}
          sort={sort}
          type={type}
        />
        <AreaFilterResetButton
          setArea={setArea}
          buildingType={buildingType}
          sort={sort}
          type={type}
        />
      </div>
      <div className={styles.button_container}>
        <p>種類</p>
        <BuildingFilterButtons
          buildingType={buildingType}
          setBuildingType={setBuildingType}
          area={area}
          sort={sort}
          type={type}
        />
        <BuildingTypeFilterResetButton
          setBuildingType={setBuildingType}
          area={area}
          sort={sort}
          type={type}
        />
      </div>
      <div className={styles.select_container}>
        <Select
          setSort={setSort}
          area={area}
          buildingType={buildingType}
          type={type}
        />
      </div>
    </div>
  );
};

export default FilterAndResetButtons;
AreaFilterButtons.tsx
"use client";

import { areaArray } from "@/app/options/options";
import Link from "next/link";

import styles from "./AreaFilterButtons.module.css";

interface AreaFilterButtonsProps {
  area: string | null;
  setArea: React.Dispatch<React.SetStateAction<string | null>>;
  buildingType: string | null;
  sort: string;
  type: string;
}

const AreaFilterButtons = ({
  area,
  setArea,
  buildingType,
  sort,
  type,
}: AreaFilterButtonsProps) => {
  return (
    <div className={styles.wrapper}>
      {areaArray.map((_area) => (
        <button
          key={_area.value}
          onClick={() => setArea(_area.value)}
          className={`${area === _area.value && styles["active"]}`}
        >
          <Link
            href={`${type}?area=${_area.value}&buildingType=${buildingType}&sort=${sort}`}
            scroll={false}
          >
            {_area.label}
          </Link>
        </button>
      ))}
    </div>
  );
};

export default AreaFilterButtons;
options.tsx
// 地区
export const areaArray = [
  { value: "mitsune", label: "三根" },
  { value: "okago", label: "大賀郷" },
  { value: "kashitate", label: "樫立" },
  { value: "nakanogo", label: "中之郷" },
  { value: "sueyoshi", label: "末吉" },
  { value: "others", label: "その他" },
];

// 建物種別
export const buildingTypeArray = [
  { value: "apartment", label: "アパート" },
  { value: "house", label: "一戸" },
  { value: "terrace", label: "テラス" },
  { value: "land", label: "土地" },
  { value: "shop", label: "店舗" },
  { value: "others", label: "その他" },
];

// 並べ替え
export const options = [
  { value: "recommendation", label: "おすすめ順" },
  { value: "new", label: "新着順" },
  { value: "cheap", label: "低価格順" },
  { value: "expensive", label: "高価格順" },
  { value: "year", label: "築年数が新しい順" },
  { value: "space", label: "専有面積が広い順" },
];

賃貸、購入ページで物件にフィルターとソート機能を実装しました。
フィルター、ソート時のページ読み込みも早くNext.jsらしさが出ていて気に入っています。
また、コードの可読性を上げるためにAreaFilterButtons.tsxにボタンのテキストを直書きするのでなくoptions.tsxに分割し、map関数で展開しています。

苦労した、解決できなかった部分

詳細ページのSwiperに関するwarning解消

Swiperを導入した詳細ページにて画像をスライドさせると下のようなwarningがDevelopToolに表示された。

調べてみると同じことが起きている人が結構いるようでそのほとんどの人が解消されないままでした。
私も解消できなかったのですが試してみたことを挙げておきます。

フォントや設定の見直し

https://github.com/vercel/next.js/discussions/49607
上記チャットを参考にしてみましたが、どれもうまくいきませんでした。

Swiperで用いる画像のpriorityについて

最初はSwiperの画像すべてのImageコンポーネントにpriorityを指定していましたが、最初の1枚目にのみpriorityを指定し、それ以降の画像にはloading={"lazy"}で遅延ロードを掛けるように修正しました。
なお今回はNext.js15を使用していますが、最新の16ではpriorityというプロパティは非推奨となり、代わりにpreloadというプロパティが追加されています。

DetailImgs.tsx
..............
    {/* 1枚目の画像にはpriorityをつける */}
        {url.includes("-1.png") ? (
            <Image
              className={styles.slide}
              src={url}
              alt={url}
              width={0}
              height={0}
              sizes="100vw"
              style={{ width: "100%", height: "auto" }}
              priority
            />
          ) : (
            <Image
              className={styles.slide}
              src={url}
              alt={url}
              width={0}
              height={0}
              sizes="100vw"
              style={{ width: "100%", height: "auto" }}
              loading={"lazy"}
            />
          )}
..............

GoogleMapに関するwarning解消

google.maps.Markerのサポートが終了しているので高度なマーカーに移行してください、ということを言っているwarningです。
もちろんドキュメントを読みながら移行作業を行ったのですが、移行用以外のドキュメントが旧マーカーのもので書かれているものが多く、移行を見送りました。

最後に

今回は初めてお仕事としてプログラミングを行いました。
複数のwarningが解消できず、非常に申し訳なかったです。
精進が必要だと感じました。

Discussion