Repository: SashenJayathilaka/Airbnb-Build Branch: master Commit: b54ed4c5775a Files: 84 Total size: 111.9 KB Directory structure: gitextract_jiqew7tf/ ├── .eslintrc.json ├── .gitignore ├── .vscode/ │ └── settings.json ├── README.md ├── app/ │ ├── actions/ │ │ ├── getCurrentUser.ts │ │ ├── getFavoriteListings.ts │ │ ├── getListingById.ts │ │ ├── getListings.ts │ │ └── getReservations.ts │ ├── api/ │ │ ├── favorites/ │ │ │ └── [listingId]/ │ │ │ └── route.ts │ │ ├── listings/ │ │ │ ├── [listingId]/ │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── register/ │ │ │ └── route.ts │ │ └── reservations/ │ │ ├── [reservationId]/ │ │ │ └── route.ts │ │ └── route.ts │ ├── error.tsx │ ├── favorites/ │ │ ├── FavoritesClient.tsx │ │ └── page.tsx │ ├── layout.tsx │ ├── listings/ │ │ └── [listingId]/ │ │ └── page.tsx │ ├── loading.tsx │ ├── page.tsx │ ├── properties/ │ │ ├── PropertiesClient.tsx │ │ └── page.tsx │ ├── reservations/ │ │ ├── ReservationsClient.tsx │ │ └── page.tsx │ └── trips/ │ ├── TripsClient.tsx │ └── page.tsx ├── components/ │ ├── Avatar.tsx │ ├── Button.tsx │ ├── CategoryBox.tsx │ ├── ClientOnly.tsx │ ├── Container.tsx │ ├── EmptyState.tsx │ ├── Footer.tsx │ ├── FooterColumn.tsx │ ├── Heading.tsx │ ├── HeartButton.tsx │ ├── ListingClient.tsx │ ├── Loader.tsx │ ├── Map.tsx │ ├── Offers.tsx │ ├── Sleep.tsx │ ├── ToastContainerBar.tsx │ ├── inputs/ │ │ ├── Calendar.tsx │ │ ├── CategoryInput.tsx │ │ ├── Counter.tsx │ │ ├── CountrySelect.tsx │ │ ├── ImageUpload.tsx │ │ └── Input.tsx │ ├── listing/ │ │ ├── ListingCard.tsx │ │ ├── ListingCategory.tsx │ │ ├── ListingHead.tsx │ │ ├── ListingInfo.tsx │ │ └── ListingReservation.tsx │ ├── models/ │ │ ├── LoginModal.tsx │ │ ├── Modal.tsx │ │ ├── RegisterModal.tsx │ │ ├── RentModal.tsx │ │ └── SearchModal.tsx │ └── navbar/ │ ├── Categories.tsx │ ├── Logo.tsx │ ├── MenuItem.tsx │ ├── Navbar.tsx │ ├── Search.tsx │ └── UserMenu.tsx ├── hook/ │ ├── useCountries.ts │ ├── useFavorite.ts │ ├── useLoginModal.ts │ ├── useRegisterModal.ts │ ├── useRentModal.ts │ └── useSearchModal.ts ├── lib/ │ └── prismadb.ts ├── middleware.ts ├── next.config.js ├── package.json ├── pages/ │ └── api/ │ └── auth/ │ └── [...nextauth].ts ├── postcss.config.js ├── prisma/ │ └── schema.prisma ├── public/ │ └── assets/ │ └── avatar.jfif ├── styles/ │ └── globals.css ├── tailwind.config.js ├── tsconfig.json └── types.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "extends": "next/core-web-vitals" } ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # local env files .env*.local .env # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts ================================================ FILE: .vscode/settings.json ================================================ { "typescript.tsdk": "node_modules\\typescript\\lib", "typescript.enablePromptUseWorkspaceTsdk": true } ================================================ FILE: README.md ================================================
logo # Airbnb Clone with Next.js 13!

Full Stack Airbnb Clone with Next.js 13 Tailwind-css, Prisma, MongoDB, NextAuth, Framer-motionSocial, Login (Google and Facebook), Image upload, Cloudinary CDN, Location selection, Map component, Country autocomplete, Fetching listings with server components.

![](https://img.shields.io/website-up-down-green-red/http/monip.org.svg) ![](https://img.shields.io/badge/Maintained-Yes-indigo) ![](https://img.shields.io/github/forks/SashenJayathilaka/Airbnb-Build.svg) ![](https://img.shields.io/github/stars/SashenJayathilaka/Airbnb-Build.svg) ![](https://img.shields.io/github/issues/SashenJayathilaka/Airbnb-Build) ![](https://img.shields.io/github/last-commit/SashenJayathilaka/Airbnb-Build)

View Demo · Documentation · Report Bug · Request Feature


## :notebook_with_decorative_cover: Table of Contents - [About the Project](#star2-about-the-project) - [Screenshots](#camera-screenshots) - [Tech Stack](#space_invader-tech-stack) - [Environment Variables](#key-environment-variables) - [Getting Started](#toolbox-getting-started) - [Prerequisites](#bangbang-prerequisites) - [Installation](#gear-installation) - [Run Locally](#running-run-locally) - [Deployment](#triangular_flag_on_post-deployment) - [Contact](#handshake-contact) ## :star2: About the Project ### :camera: Screenshots - Reservation functionality & Description and Price, Listing creation, Listing card component
image

- Searching functionality Favorite functionality, Individual Listing View, Listing reservation component
image
## LIVE DEMO 💥 ![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg) ![forthebadge](https://forthebadge.com/images/badges/for-you.svg) ![forthebadge](https://forthebadge.com/images/badges/powered-by-coffee.svg) ### :space_invader: Tech Stack
Client
Database

Google
## :toolbox: Getting Started ### :bangbang: Prerequisites - Install Node JS in your computer HERE - Sign up for a Cloudinary account HERE - Sign up for a Google Cloud Platform HERE - Sign up for a Meta for Developers HERE - Get Lookup APi Key HERE ### :key: Environment Variables To run this project, you will need to add the following environment variables to your .env file `DATABASE_URL` `GOOGLE_CLIENT_ID` `GOOGLE_CLIENT_SECRET` `FACEBOOK_ID` `FACEBOOK_SECRET` `NEXTAUTH_SECRET` `NEXTAUTH_URL` `NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME` `NEXT_PUBLIC_LOOKUP_KEY` This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). ### :gear: Installation ![](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB) ![](https://img.shields.io/badge/next.js-20232A?style=for-the-badge&logo=next.js&logoColor=61DAFB) Install my-project with npm ``` npx create-next-app@latest my-project --typescript --eslint ``` ``` cd my-project ``` Install dependencies ### :test_tube: Install Tailwind CSS with Next.js #### Install Tailwind CSS ![](https://img.shields.io/badge/Tailwind_CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white) Install tailwindcss and its peer dependencies via npm, and then run the init command to generate both `tailwind.config.js` and `postcss.config.js`. ``` npm install -D tailwindcss postcss autoprefixer ``` ``` npx tailwindcss init -p ``` #### Configure your template paths Add the paths to all of your template files in your `tailwind.config.js` file.
```js /** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./app/**/*.{js,ts,jsx,tsx}", "./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}", // Or if using `src` directory: "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [], }; ``` #### Add the Tailwind directives to your CSS Add the `@tailwind` directives for each of Tailwind’s layers to your `./styles/globals.css` file. ```css @tailwind base; @tailwind components; @tailwind utilities; ``` Install dependencies 🔶 Dependency Info ### :running: Run Locally ![](https://img.shields.io/badge/GIT-E44C30?style=for-the-badge&logo=git&logoColor=white) Clone the project ```bash git clone https://github.com/SashenJayathilaka/Airbnb-Build.git ``` change directory ```bash cd Airbnb-Build ``` Install dependencies ```bash npm install ``` Start the server ```bash npm run dev ```
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. ### Learn More To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! ### :triangular_flag_on_post: Deployment To deploy this project run ##### Deploy on Vercel ![](https://img.shields.io/badge/Vercel-000000?style=for-the-badge&logo=vercel&logoColor=white) The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. ## :handshake: Contact Sashen - [@twitter_handle](https://twitter.com/SashenHasinduJ) - sashenjayathilaka95@gmail.com Project Link: [https://github.com/SashenJayathilaka/Airbnb-Build.git](https://github.com/SashenJayathilaka/Airbnb-Build.git)
image

Don't forget to leave a star ⭐️
================================================ FILE: app/actions/getCurrentUser.ts ================================================ import prisma from "@/lib/prismadb"; import { authOptions } from "@/pages/api/auth/[...nextauth]"; import { getServerSession } from "next-auth/next"; export async function getSession() { return await getServerSession(authOptions); } export default async function getCurrentUser() { try { const session = await getSession(); if (!session?.user?.email) { return null; } const currentUser = await prisma.user.findUnique({ where: { email: session.user.email as string, }, }); if (!currentUser) { return null; } return { ...currentUser, createdAt: currentUser.createdAt.toISOString(), updatedAt: currentUser.updatedAt.toISOString(), emailVerified: currentUser.emailVerified?.toISOString() || null, }; } catch (error: any) { console.log( "🚀 ~ file: getCurrentUser.ts:13 ~ getCurrentUser ~ error:", error ); } } ================================================ FILE: app/actions/getFavoriteListings.ts ================================================ import prisma from "@/lib/prismadb"; import getCurrentUser from "./getCurrentUser"; export default async function getFavoriteListings() { try { const currentUser = await getCurrentUser(); if (!currentUser) { return []; } const favorites = await prisma.listing.findMany({ where: { id: { in: [...(currentUser.favoriteIds || [])], }, }, }); const safeFavorite = favorites.map((favorite) => ({ ...favorite, createdAt: favorite.createdAt.toString(), })); return safeFavorite; } catch (error: any) { throw new Error(error.message); } } ================================================ FILE: app/actions/getListingById.ts ================================================ import prisma from "@/lib/prismadb"; interface IParams { listingId?: string; } export default async function getListingById(params: IParams) { try { const { listingId } = params; const listing = await prisma.listing.findUnique({ where: { id: listingId, }, include: { user: true, }, }); if (!listing) { return null; } return { ...listing, createdAt: listing.createdAt.toString(), user: { ...listing.user, createdAt: listing.user.createdAt.toString(), updatedAt: listing.user.updatedAt.toString(), emailVerified: listing.user.emailVerified?.toString() || null, }, }; } catch (error: any) { throw new Error(error.message); } } ================================================ FILE: app/actions/getListings.ts ================================================ import prisma from "@/lib/prismadb"; export interface IListingsParams { userId?: string; guestCount?: number; roomCount?: number; bathroomCount?: number; startDate?: string; endDate?: string; locationValue?: string; category?: string; } export default async function getListings(params: IListingsParams) { try { const { userId, roomCount, guestCount, bathroomCount, locationValue, startDate, endDate, category, } = params; let query: any = {}; if (userId) { query.userId = userId; } if (category) { query.category = category; } if (roomCount) { query.roomCount = { gte: +roomCount, }; } if (guestCount) { query.guestCount = { gte: +guestCount, }; } if (bathroomCount) { query.bathroomCount = { gte: +bathroomCount, }; } if (locationValue) { query.locationValue = locationValue; } if (startDate && endDate) { query.NOT = { reservations: { some: { OR: [ { endDate: { gte: startDate }, startDate: { lte: startDate }, }, { startDate: { lte: endDate }, endDate: { gte: endDate }, }, ], }, }, }; } const listing = await prisma.listing.findMany({ where: query, orderBy: { createdAt: "desc", }, }); const safeListings = listing.map((list) => ({ ...list, createdAt: list.createdAt.toISOString(), })); return safeListings; } catch (error: any) { throw new Error(error.message); } } ================================================ FILE: app/actions/getReservations.ts ================================================ import prisma from "@/lib/prismadb"; interface IParams { listingId?: string; userId?: string; authorId?: string; } export default async function getReservation(params: IParams) { try { const { listingId, userId, authorId } = params; const query: any = {}; if (listingId) { query.listingId = listingId; } if (userId) { query.userId = userId; } if (authorId) { query.listing = { userId: authorId }; } const reservation = await prisma.reservation.findMany({ where: query, include: { listing: true, }, orderBy: { createdAt: "desc", }, }); const safeReservations = reservation.map((reservation) => ({ ...reservation, createdAt: reservation.createdAt.toISOString(), startDate: reservation.startDate.toISOString(), endDate: reservation.endDate.toISOString(), listing: { ...reservation.listing, createdAt: reservation.listing.createdAt.toISOString(), }, })); return safeReservations; } catch (error: any) { throw new Error(error.message); } } ================================================ FILE: app/api/favorites/[listingId]/route.ts ================================================ import getCurrentUser from "@/app/actions/getCurrentUser"; import prisma from "@/lib/prismadb"; import { NextResponse } from "next/server"; interface IPrisma { listingId?: string; } export async function POST(request: Request, { params }: { params: IPrisma }) { const currentUser = await getCurrentUser(); if (!currentUser) { return NextResponse.error(); } const { listingId } = params; if (!listingId || typeof listingId !== "string") { throw new Error("Invalid Id"); } let favoriteIds = [...(currentUser.favoriteIds || [])]; favoriteIds.push(listingId); const user = await prisma.user.update({ where: { id: currentUser.id, }, data: { favoriteIds, }, }); return NextResponse.json(user); } export async function DELETE( request: Request, { params }: { params: IPrisma } ) { const currentUser = await getCurrentUser(); if (!currentUser) { return NextResponse.error(); } const { listingId } = params; if (!listingId || typeof listingId !== "string") { throw new Error("Invalid Id"); } let favoriteIds = [...(currentUser.favoriteIds || [])]; favoriteIds = favoriteIds.filter((id) => id !== listingId); const user = await prisma.user.update({ where: { id: currentUser.id, }, data: { favoriteIds, }, }); return NextResponse.json(user); } ================================================ FILE: app/api/listings/[listingId]/route.ts ================================================ import { NextResponse } from "next/server"; import getCurrentUser from "@/app/actions/getCurrentUser"; import prisma from "@/lib/prismadb"; interface IParams { listingId?: string; } export async function DELETE( request: Request, { params }: { params: IParams } ) { const currentUser = await getCurrentUser(); if (!currentUser) { return NextResponse.error(); } const { listingId } = params; if (!listingId || typeof listingId !== "string") { throw new Error("Invalid Id"); } const listing = await prisma.listing.deleteMany({ where: { id: listingId, userId: currentUser.id, }, }); return NextResponse.json(listing); } ================================================ FILE: app/api/listings/route.ts ================================================ import getCurrentUser from "@/app/actions/getCurrentUser"; import prisma from "@/lib/prismadb"; import { NextResponse } from "next/server"; export async function POST(request: Request) { const currentUser = await getCurrentUser(); if (!currentUser) { return NextResponse.error(); } const body = await request.json(); const { title, description, imageSrc, category, roomCount, bathroomCount, guestCount, location, price, } = body; Object.keys(body).forEach((value: any) => { if (!body[value]) { NextResponse.error(); } }); const listen = await prisma.listing.create({ data: { title, description, imageSrc, category, roomCount, bathroomCount, guestCount, locationValue: location.value, price: parseInt(price, 10), userId: currentUser.id, }, }); return NextResponse.json(listen); } ================================================ FILE: app/api/register/route.ts ================================================ import prisma from "@/lib/prismadb"; import bcrypt from "bcrypt"; import { NextResponse } from "next/server"; export async function POST(request: Request) { const body = await request.json(); const { email, name, password } = body; const hashedPassword = await bcrypt.hash(password, 12); const user = await prisma.user.create({ data: { email, name, hashedPassword, }, }); return NextResponse.json(user); } ================================================ FILE: app/api/reservations/[reservationId]/route.ts ================================================ import getCurrentUser from "@/app/actions/getCurrentUser"; import prisma from "@/lib/prismadb"; import { NextResponse } from "next/server"; interface IParams { reservationId?: string; } export async function DELETE( request: Request, { params }: { params: IParams } ) { const currentUser = await getCurrentUser(); if (!currentUser) { return NextResponse.error(); } const { reservationId } = params; if (!reservationId || typeof reservationId !== "string") { throw new Error("Invalid Id"); } const reservation = await prisma.reservation.deleteMany({ where: { id: reservationId, OR: [{ userId: currentUser.id }, { listing: { userId: currentUser.id } }], }, }); return NextResponse.json(reservation); } ================================================ FILE: app/api/reservations/route.ts ================================================ import getCurrentUser from "@/app/actions/getCurrentUser"; import prisma from "@/lib/prismadb"; import { NextResponse } from "next/server"; export async function POST(request: Request) { const currentUser = await getCurrentUser(); if (!currentUser) { return NextResponse.error(); } const body = await request.json(); const { listingId, startDate, endDate, totalPrice } = body; if (!listingId || !startDate || !endDate || !totalPrice) { return NextResponse.error(); } const listenAndReservation = await prisma.listing.update({ where: { id: listingId, }, data: { reservations: { create: { userId: currentUser.id, startDate, endDate, totalPrice, }, }, }, }); return NextResponse.json(listenAndReservation); } ================================================ FILE: app/error.tsx ================================================ "use client"; import EmptyState from "@/components/EmptyState"; import { useEffect } from "react"; type Props = { error: Error; }; function ErrorState({ error }: Props) { useEffect(() => { console.log("🚀 ~ file: error.tsx:12 ~ ErrorState ~ error:", error); }, [error]); return ; } export default ErrorState; ================================================ FILE: app/favorites/FavoritesClient.tsx ================================================ import Container from "@/components/Container"; import Heading from "@/components/Heading"; import ListingCard from "@/components/listing/ListingCard"; import { SafeUser, safeListing } from "@/types"; type Props = { listings: safeListing[]; currentUser?: SafeUser | null; }; function FavoritesClient({ listings, currentUser }: Props) { return (
{listings.map((listing) => ( ))}
); } export default FavoritesClient; ================================================ FILE: app/favorites/page.tsx ================================================ import ClientOnly from "@/components/ClientOnly"; import EmptyState from "@/components/EmptyState"; import React from "react"; import getCurrentUser from "../actions/getCurrentUser"; import getFavoriteListings from "../actions/getFavoriteListings"; import FavoritesClient from "./FavoritesClient"; type Props = {}; const FavoritePage = async (props: Props) => { const currentUser = await getCurrentUser(); const listings = await getFavoriteListings(); if (!currentUser) { return ( ); } if (listings.length === 0) { return ( ); } return ( ); }; export default FavoritePage; ================================================ FILE: app/layout.tsx ================================================ import ClientOnly from "@/components/ClientOnly"; import Footer from "@/components/Footer"; import ToastContainerBar from "@/components/ToastContainerBar"; import LoginModal from "@/components/models/LoginModal"; import RegisterModal from "@/components/models/RegisterModal"; import RentModal from "@/components/models/RentModal"; import SearchModal from "@/components/models/SearchModal"; import Navbar from "@/components/navbar/Navbar"; import { Nunito } from "next/font/google"; import "../styles/globals.css"; import getCurrentUser from "./actions/getCurrentUser"; export const metadata = { title: "Airbnb Clone", description: "Airbnb Clone", icons: "https://www.seekpng.com/png/full/957-9571167_airbnb-png.png", }; const font = Nunito({ subsets: ["latin"], }); export default async function RootLayout({ children, }: { children: React.ReactNode; }) { const currentUser = await getCurrentUser(); return (
{children}