/home-away

a Next.js application from scratch, understanding its structure, and mastering advanced routing techniques, including link components and dynamic paths.

Primary LanguageTypeScript

HomeAway

The project is an Airbnb clone built using Next.js, designed to simulate the features of the Airbnb rental platform. The project built using Next.js, including server-side rendering, static site generation, and dynamic routing with a focus on showcasing properties, searching, and filtering. Designed as part of a course to demonstrate practical skills in building a real-world application with Next.js. Key features include:

  • Property Listings: Displays a range of vacation rental properties with detailed information.
  • Filter and Search: Users can filter listings by type of property and search for properties.
  • A booking system integrated.
  • Dynamic Routing: Each property has its own page with detailed information.
  • Data Fetching: Utilizes Next.js's data-fetching methods for server-side rendering.
  • User Interaction: Includes form handling and interactive UI elements.
  • Responsive Design: The application is optimized for both desktop and mobile devices.

Table of contents

Overview

The challenge

  • Data Management: Handling data fetching and displaying dynamic content efficiently.
  • Dynamic Routing: Implementing dynamic routes to handle various property listings and details.
  • State Management: Handling complex state management for filtering and displaying the property listings.
  • Server-Side Rendering (SSR): Managing SSR to ensure that data is pre-rendered for faster load times and improved SEO.
  • Complex Layouts: Implementing complex, responsive layouts that work across multiple devices and screen sizes.
  • API Integration: Fetching and displaying data from external sources or APIs and handling errors gracefully.
  • Integrating third-party services for payment processing and authentication.

Screenshot

Links

Live site URL

My process

Built with

Here's a unified version of the "What I Learned" section:

What I Learned

  • Next.js Fundamentals: Setting up and configuring a Next.js project.
  • Dynamic Routing: Implementing and managing dynamic routes in a Next.js application.
  • Server-Side Rendering: Using Next.js’s data-fetching methods to render pages on the server.
  • State Management: Approaches for managing and filtering state in a React and Next.js application.
  • Data Fetching Techniques: Best practices for fetching data and handling asynchronous operations in a Next.js environment.
  • API Integration: Integrating APIs, fetching data, and managing asynchronous operations.
  • Payment Integration: Handling payment processing and secure transactions using Stripe.
  • User Authentication: Implementing user authentication with Clerk.
  • Data Validation: Using Zod for building schemas and validating data with type safety and error handling.
  • Database Management: Working with Supabase and Prisma for database management and operations.

create .env.local

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
import { ClerkProvider } from "@clerk/nextjs";

return (
  <ClerkProvider>
    <html lang="en" suppressHydrationWarning>
      <body className={inter.className}>
        <Providers>
          <Navbar />
          <main className="container py-10">{children}</main>
        </Providers>
      </body>
    </html>
  </ClerkProvider>
);

create middleware.ts

import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";

const isProtectedRoute = createRouteMatcher([
  "/bookings(.*)",
  "/checkout(.*)",
  "/favorites(.*)",
  "/profile(.*)",
  "/rentals(.*)",
  "/reviews(.*)",
]);

export default clerkMiddleware((auth, req) => {
  if (isProtectedRoute(req)) auth().protect();
});

export const config = {
  matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};

Remote Patterns

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "img.clerk.com",
      },
      {
        protocol: "https",
        hostname: "jxdujzgweuaphpgoowhu.supabase.co",
      },
    ],
  },
};

export default nextConfig;

Zod

is a JavaScript library for building schemas and validating data, providing type safety and error handling.

  • create utils/schemas.ts
import * as z from "zod";
import { ZodSchema } from "zod";

export const profileSchema = z.object({
  // firstName: z.string().max(5, { message: 'max length is 5' }),
  firstName: z.string(),
  lastName: z.string(),
  username: z.string(),
});
  • create utils/actions.ts
  • import in profile/create page.tsx
"use server";

import { profileSchema } from "./schemas";

export const createProfileAction = async (
  prevState: any,
  formData: FormData
) => {
  try {
    const rawData = Object.fromEntries(formData);
    const validatedFields = profileSchema.parse(rawData);
    console.log(validatedFields);
    return { message: "Profile Created" };
  } catch (error) {
    console.log(error);
    return { message: "there was an error..." };
  }
};

In development, the command next dev clears Node.js cache on run. This in turn initializes a new PrismaClient instance each time due to hot reloading that creates a connection to the database. This can quickly exhaust the database connections as each PrismaClient instance holds its own connection pool.

  • create utils/db.ts
import { PrismaClient } from "@prisma/client";

const prismaClientSingleton = () => {
  return new PrismaClient();
};

type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>;

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClientSingleton | undefined;
};

const prisma = globalForPrisma.prisma ?? prismaClientSingleton();

export default prisma;

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

Connect Supabase with Prisma

Useful Info Prisma Docs

  • add to .env
DATABASE_URL=""
DIRECT_URL=""
  • DATABASE_URL : Transaction + Password + "?pgbouncer=true&connection_limit=1"
  • DIRECT_URL : Session + Password
datasource db {
  provider          = "postgresql"
  url               = env("DATABASE_URL")
  directUrl         = env("DIRECT_URL")
}

generator client {
  provider = "prisma-client-js"
}

model TestProfile {
id  String @id @default(uuid())
name String
}

(creates a new migration for your database schema)

npx prisma migrate dev --name init
npx prisma db push
npx prisma studio

Prisma's findUnique and findFirst methods are used to retrieve a single record from the database, but they have some differences in their behavior:

  • findUnique: This method is used when you want to retrieve a single record that matches a unique constraint or a primary key. If no record is found, it returns null.

  • findFirst: This method is used when you want to retrieve a single record that matches a non-unique constraint. It can also be used with ordering and filtering. If no record is found, it returns null.

In summary, use findUnique when you're sure the field you're querying by is unique, and use findFirst when you're querying by a non-unique field or need more complex queries with ordering and filtering.

const user = await prisma.user.findUnique({
  where: {
    email: "alice@prisma.io",
  },
});

const user = await prisma.user.findFirst({
  where: {
    email: {
      contains: "prisma.io",
    },
  },
  orderBy: {
    name: "asc",
  },
});

Setup Supabase

npm install @supabase/supabase-js

utils/supabase.ts

import { createClient } from "@supabase/supabase-js";

const bucket = "home-away-draft";

// Create a single supabase client for interacting with your database
export const supabase = createClient(
  process.env.SUPABASE_URL as string,
  process.env.SUPABASE_KEY as string
);

export const uploadImage = async (image: File) => {
  const timestamp = Date.now();
  // const newName = `/users/${timestamp}-${image.name}`;
  const newName = `${timestamp}-${image.name}`;

  const { data, error } = await supabase.storage
    .from(bucket)
    .upload(newName, image, {
      cacheControl: "3600",
    });
  if (!data) throw new Error("Image upload failed");
  return supabase.storage.from(bucket).getPublicUrl(newName).data.publicUrl;
};
"use client";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import { Button } from "../ui/button";
import { LuShare2 } from "react-icons/lu";

import {
  TwitterShareButton,
  EmailShareButton,
  LinkedinShareButton,
  TwitterIcon,
  EmailIcon,
  LinkedinIcon,
} from "react-share";

function ShareButton({
  propertyId,
  name,
}: {
  propertyId: string;
  name: string;
}) {
  const url = process.env.NEXT_PUBLIC_WEBSITE_URL;
  const shareLink = `${url}/properties/${propertyId}`;

  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline" size="icon" className="p-2">
          <LuShare2 />
        </Button>
      </PopoverTrigger>
      <PopoverContent
        side="top"
        align="end"
        sideOffset={10}
        className="flex items-center gap-x-2 justify-center w-full"
      >
        <TwitterShareButton url={shareLink} title={name}>
          <TwitterIcon size={32} round />
        </TwitterShareButton>
        <LinkedinShareButton url={shareLink} title={name}>
          <LinkedinIcon size={32} round />
        </LinkedinShareButton>
        <EmailShareButton url={shareLink} subject={name}>
          <EmailIcon size={32} round />
        </EmailShareButton>
      </PopoverContent>
    </Popover>
  );
}
export default ShareButton;

Leaflet makes direct calls to the DOM when it is loaded, therefore React Leaflet is not compatible with server-side rendering.

npm install react react-dom leaflet react-leaflet
npm install -D @types/leaflet
  • components/properties/PropertyMap.tsx
"use client";
import { MapContainer, TileLayer, Marker, ZoomControl } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import { icon } from "leaflet";
const iconUrl =
  "https://unpkg.com/leaflet@1.9.3/dist/images/marker-icon-2x.png";
const markerIcon = icon({
  iconUrl: iconUrl,
  iconSize: [20, 30],
});

import { findCountryByCode } from "@/utils/countries";
import CountryFlagAndName from "../card/CountryFlagAndName";
import Title from "./Title";

function PropertyMap({ countryCode }: { countryCode: string }) {
  const defaultLocation = [51.505, -0.09] as [number, number];
  const location = findCountryByCode(countryCode)?.location as [number, number];

  return (
    <div className="mt-4">
      <div className="mb-4 ">
        <Title text="Where you will be staying" />
        <CountryFlagAndName countryCode={countryCode} />
      </div>
      <MapContainer
        scrollWheelZoom={false}
        zoomControl={false}
        className="h-[50vh] rounded-lg relative z-0"
        center={location || defaultLocation}
        zoom={7}
      >
        <TileLayer
          attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        <ZoomControl position="bottomright" />
        <Marker
          position={location || defaultLocation}
          icon={markerIcon}
        ></Marker>
      </MapContainer>
    </div>
  );
}
export default PropertyMap;

Blocked Periods/Dates

BookingCalendar.tsx

function BookingCalendar() {
  const bookings = useProperty((state) => state.bookings);
  const blockedPeriods = generateBlockedPeriods({
    bookings,
    today: currentDate,
  });

  return (
    <Calendar
      mode="range"
      defaultMonth={currentDate}
      selected={range}
      onSelect={setRange}
      className="mb-4"
      // add disabled
      disabled={blockedPeriods}
    />
  );
}
export default BookingCalendar;

How to seet up Admin User - Middleware

  • refactor middleware
  • create ENV variable with userId
  • add to VERCEL
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";

import { NextResponse } from "next/server";

const isPublicRoute = createRouteMatcher(["/", "/properties(.*)"]);

const isAdminRoute = createRouteMatcher(["/admin(.*)"]);
export default clerkMiddleware(async (auth, req) => {
  const isAdminUser = auth().userId === process.env.ADMIN_USER_ID;
  if (isAdminRoute(req) && !isAdminUser) {
    return NextResponse.redirect(new URL("/", req.url));
  }
  if (!isPublicRoute(req)) auth().protect();
});

export const config = {
  matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};

Continued development

  • Enhanced UI/UX: Adding more interactive features and improving the user interface.
  • Performance Optimization: Implementing performance enhancements and optimizations.
  • Enhanced Filtering Options: Adding more sophisticated filtering criteria for better user experience.
  • Testing: Implementing comprehensive testing strategies to ensure robustness and reliability.
  • Filter: Implementing more advanced search and filter functionalities.

Useful resources

Certainly! Here's a unified list of useful resources with a consistent format:

Useful Resources

  • Next.js Examples: Next.js Examples – Examples of how to use Next.js for various use cases.
  • React Hooks: React Hooks Documentation – Official documentation on using hooks in React for state and lifecycle management.
  • Frontend Performance Tips: Web.dev Performance Tips – Tips and best practices for optimizing frontend performance.
  • React Icons: React Icons – A collection of popular icons to use in React applications.
  • UI Themes: Themes – Resources for applying themes to your web applications.
  • Next.js Dark Mode: Next.js Dark Mode – Guide to implementing dark mode in Next.js applications.
  • Clerk + Next.js Setup: Clerk + Next.js Setup – Quickstart guide for setting up Clerk with Next.js.
  • Clerk User Metadata: Clerk User Metadata – Documentation on managing user metadata with Clerk.
  • Supabase Integration: Supabase Info – Information on integrating Supabase with Prisma.
  • Prisma Docs: Prisma Docs – Documentation on using Prisma for database operations.
  • Prisma in Next.js: Prisma Instance – Best practices for using Prisma in a Next.js project.
  • React Share: React Share – Library for adding social sharing buttons in React applications.
  • React Leaflet: React Leaflet – Documentation for using Leaflet maps in React.
  • Recharts: Zustand Docs – Documentation for Recharts library for charts.
  • Zustand Docs: Zustand Docs – Documentation for Zustand, a small, fast state-management library for React.

Acknowledgments

  • John Smilga: For providing the foundational project structure and guidance through the course materials.