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
================================================
<div align="center">
<img src="https://user-images.githubusercontent.com/99184393/185779974-a31a9f47-f8d3-42ea-b7f8-4a2971774615.png" alt="logo" width="250" height="auto" />
# Airbnb Clone with Next.js 13!
<p>
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.
</p>
<!-- Badges -->
<a href="https://airbnb-sclone.vercel.app" target="_blank"></a>





<h4>
<a href="https://abproject-sclone.vercel.app">View Demo</a>
<span> · </span>
<a href="https://github.com/SashenJayathilaka/Airbnb-Build/blob/master/README.md">Documentation</a>
<span> · </span>
<a href="https://github.com/SashenJayathilaka/Airbnb-Build/issues">Report Bug</a>
<span> · </span>
<a href="https://github.com/SashenJayathilaka/Airbnb-Build/issues">Request Feature</a>
</h4>
</div>
<br />
<!-- Table of Contents -->
## :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)
<!-- About the Project -->
## :star2: About the Project
<!-- Screenshots -->
### :camera: Screenshots
- Reservation functionality & Description and Price, Listing creation, Listing card component
<div align="center">
<a href="https://airbnb-sclone.vercel.app"><img src='./demo/ezgif-4-27f1be5f56.gif' alt='image'/></a>
</div>
<br />
- Searching functionality Favorite functionality, Individual Listing View, Listing reservation component
<div align="center">
<a href="https://airbnb-sclone.vercel.app"><img src='./demo/ezgif-4-8ac9db77ff.gif' alt='image'/></a>
</div>
## <a href="https://airbnb-sclone.vercel.app" target="_blank">LIVE DEMO 💥</a>



### :space_invader: Tech Stack
<details>
<summary>Client</summary>
<ul>
<li><a href="https://#/">Typescript</a></li>
<li><a href="https://nextjs.org/">Next.js</a></li>
<li><a href="https://reactjs.org/">React.js</a></li>
<li><a href="https://tailwindcss.com/">TailwindCSS</a></li>
<li><a href="https://www.prisma.io">Prisma</a></li>
</ul>
</details>
<details>
<summary>Database</summary>
<ul>
<li><a href="https://firebase.google.com">Mongodb</a></li>
<li><a href="https://cloudinary.com/">Cloudinary</a></li>
</ul>
</details>
<br />
<table>
<tr>
<td>
<a href="#"><img src="https://raw.githubusercontent.com/devicons/devicon/master/icons/react/react-original.svg" alt="" width="30" height="30" /></a>
</td>
<td>
<a href="#"><img src="https://user-images.githubusercontent.com/99184393/183096870-fdf58e59-d78c-44f4-bd1c-f9033c16d907.png" alt="Google" width="30" height="30" /></a>
</td>
<td>
<a href="#"><img src="https://user-images.githubusercontent.com/99184393/179383376-874f547c-4e6f-4826-850e-706b009e7e2b.png" alt="" width="30" height="30" /></a>
</td>
<td>
<a href="#"><img src="https://user-images.githubusercontent.com/99184393/181918664-569af962-756c-438c-b350-294f042e6f61.png" alt="" width="30" height="30" /></a>
</td>
<td>
<a href="#"><img src="https://user-images.githubusercontent.com/99184393/180462270-ea4a249c-627c-4479-9431-5c3fd25454c4.png" alt="" width="30" height="30" /></a>
</td>
<td>
<a href="#"><img src="https://user-images.githubusercontent.com/99184393/185779974-a31a9f47-f8d3-42ea-b7f8-4a2971774615.png" alt="" width="30"height="30"/></a>
</td>
<td>
<a href="#"><img src="https://user-images.githubusercontent.com/99184393/229775276-a7cb148b-7fbd-4334-a07f-f2223bc49f62.png" alt="" width="30"height="30"/></a>
</td>
<td>
<a href="#"><img src="https://user-images.githubusercontent.com/99184393/204170976-0e5c6e2a-2b41-483d-adbd-d5d1e40b8d15.png" alt="" width="30"height="30"/></a>
</td>
<td>
<a href="#"><img src="https://user-images.githubusercontent.com/99184393/214867309-7b59fa0e-c872-484e-bc8f-462896c54d2a.png" alt="" height="30"/></a>
</td>
</tr>
</table>
## :toolbox: Getting Started
### :bangbang: Prerequisites
- Install Node JS in your computer <a href='https://nodejs.org/en/'>HERE</a>
- Sign up for a Cloudinary account <a href='https://cloudinary.com/'>HERE</a>
- Sign up for a Google Cloud Platform <a href='https://console.cloud.google.com/'>HERE</a>
- Sign up for a Meta for Developers <a href='https://developers.facebook.com'>HERE</a>
- Get Lookup APi Key <a href='https://extreme-ip-lookup.com/'>HERE</a>
<!-- Env Variables -->
### :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


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

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.
<br>
```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
<a href="https://github.com/SashenJayathilaka/Airbnb-Build/blob/master/package.json" target="_blank">🔶 Dependency Info</a>
<!-- Run Locally -->
### :running: Run Locally

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
```
<hr />
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).
<hr />
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!
<!-- Deployment -->
### :triangular_flag_on_post: Deployment
To deploy this project run
##### Deploy on Vercel

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)
<br />
<div align="center">
<a href="https://airbnb-sclone.vercel.app"><img src='https://user-images.githubusercontent.com/99184393/229773559-72e7f64a-361d-4285-976a-00a8919dd783.png' alt='image'/></a>
</div>
<br />
<div align="center">Don't forget to leave a star ⭐️</div>
================================================
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 <EmptyState title="Uh Oh" subtitle="Something went wrong!" />;
}
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 (
<Container>
<Heading title="Favorites" subtitle="List of places you favorites!" />
<div className="mt-10 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-5 gap-8">
{listings.map((listing) => (
<ListingCard
currentUser={currentUser}
key={listing.id}
data={listing}
/>
))}
</div>
</Container>
);
}
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 (
<ClientOnly>
<EmptyState title="Unauthorized" subtitle="Please login" />
</ClientOnly>
);
}
if (listings.length === 0) {
return (
<ClientOnly>
<EmptyState
title="No favorites found"
subtitle="Looks like you have no favorite listings."
/>
</ClientOnly>
);
}
return (
<ClientOnly>
<FavoritesClient listings={listings} currentUser={currentUser} />
</ClientOnly>
);
};
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 (
<html lang="en">
<body className={font.className}>
<ClientOnly>
<ToastContainerBar />
<SearchModal />
<RegisterModal />
<LoginModal />
<RentModal />
<Navbar currentUser={currentUser} />
</ClientOnly>
<div className="pb-20 pt-28">{children}</div>
<Footer />
</body>
</html>
);
}
================================================
FILE: app/listings/[listingId]/page.tsx
================================================
import getCurrentUser from "@/app/actions/getCurrentUser";
import getListingById from "@/app/actions/getListingById";
import getReservation from "@/app/actions/getReservations";
import ClientOnly from "@/components/ClientOnly";
import EmptyState from "@/components/EmptyState";
import ListingClient from "@/components/ListingClient";
interface IParams {
listingId?: string;
}
const ListingPage = async ({ params }: { params: IParams }) => {
const listing = await getListingById(params);
const reservations = await getReservation(params);
const currentUser = await getCurrentUser();
if (!listing) {
return (
<ClientOnly>
<EmptyState />
</ClientOnly>
);
}
return (
<ClientOnly>
<ListingClient
listing={listing}
currentUser={currentUser}
reservations={reservations}
/>
</ClientOnly>
);
};
export default ListingPage;
================================================
FILE: app/loading.tsx
================================================
import Loader from "@/components/Loader";
type Props = {};
function Loading({}: Props) {
return <Loader />;
}
export default Loading;
================================================
FILE: app/page.tsx
================================================
import ClientOnly from "@/components/ClientOnly";
import Container from "@/components/Container";
import EmptyState from "@/components/EmptyState";
import ListingCard from "@/components/listing/ListingCard";
import getCurrentUser from "./actions/getCurrentUser";
import getListings, { IListingsParams } from "./actions/getListings";
interface HomeProps {
searchParams: IListingsParams;
}
export default async function Home({ searchParams }: HomeProps) {
const listing = await getListings(searchParams);
const currentUser = await getCurrentUser();
if (listing.length === 0) {
return (
<ClientOnly>
<EmptyState showReset />
</ClientOnly>
);
}
return (
<ClientOnly>
<Container>
<div className="pt-24 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-4 gap-8 overflow-x-hidden">
{listing.map((list) => {
return (
<ListingCard
key={list.id}
data={list}
currentUser={currentUser}
/>
);
})}
</div>
</Container>
</ClientOnly>
);
}
================================================
FILE: app/properties/PropertiesClient.tsx
================================================
"use client";
import Container from "@/components/Container";
import Heading from "@/components/Heading";
import ListingCard from "@/components/listing/ListingCard";
import { SafeUser, safeListing } from "@/types";
import axios from "axios";
import { useRouter } from "next/navigation";
import { useCallback, useState } from "react";
import { toast } from "react-toastify";
type Props = {
listings: safeListing[];
currentUser?: SafeUser | null;
};
function PropertiesClient({ listings, currentUser }: Props) {
const router = useRouter();
const [deletingId, setDeletingId] = useState("");
const onDelete = useCallback(
(id: string) => {
setDeletingId(id);
axios
.delete(`/api/listings/${id}`)
.then(() => {
toast.info("Listing deleted");
router.refresh();
})
.catch((error) => {
toast.error(error?.response?.data?.error);
})
.finally(() => {
setDeletingId("");
});
},
[router]
);
return (
<Container>
<Heading title="Properties" subtitle="List of your properties" />
<div className="mt-10 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-5 gap-8">
{listings.map((listing: any) => (
<ListingCard
key={listing.id}
data={listing}
actionId={listing.id}
onAction={onDelete}
disabled={deletingId === listing.id}
actionLabel="Delete property"
currentUser={currentUser}
/>
))}
</div>
</Container>
);
}
export default PropertiesClient;
================================================
FILE: app/properties/page.tsx
================================================
import ClientOnly from "@/components/ClientOnly";
import EmptyState from "@/components/EmptyState";
import getCurrentUser from "../actions/getCurrentUser";
import getListings from "../actions/getListings";
import PropertiesClient from "./PropertiesClient";
type Props = {};
const PropertiesPage = async (props: Props) => {
const currentUser = await getCurrentUser();
if (!currentUser) {
return (
<ClientOnly>
<EmptyState title="Unauthorized" subtitle="Please login" />
</ClientOnly>
);
}
const listings = await getListings({ userId: currentUser.id });
if (listings.length === 0) {
return (
<ClientOnly>
<EmptyState
title="No Properties found"
subtitle="Looks like you have not any Properties"
/>
</ClientOnly>
);
}
return (
<ClientOnly>
<PropertiesClient listings={listings} currentUser={currentUser} />
</ClientOnly>
);
};
export default PropertiesPage;
================================================
FILE: app/reservations/ReservationsClient.tsx
================================================
"use client";
import { SafeReservation, SafeUser } from "@/types";
import axios from "axios";
import { useRouter } from "next/navigation";
import React, { useCallback, useState } from "react";
import { toast } from "react-toastify";
import Container from "@/components/Container";
import Heading from "@/components/Heading";
import ListingCard from "@/components/listing/ListingCard";
type Props = {
reservations: SafeReservation[];
currentUser?: SafeUser | null;
};
function ReservationsClient({ reservations, currentUser }: Props) {
const router = useRouter();
const [deletingId, setDeletingId] = useState("");
const onCancel = useCallback(
(id: string) => {
setDeletingId(id);
axios
.delete(`/api/reservations/${id}`)
.then(() => {
toast.info("Reservation cancelled");
router.refresh();
})
.catch((error) => {
toast.error(error?.response?.data?.error);
})
.finally(() => {
setDeletingId("");
});
},
[router]
);
return (
<Container>
<Heading title="Reservations" subtitle="Bookings on your properties" />
<div className="mt-10 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-5 gap-8">
{reservations.map((reservation) => (
<ListingCard
key={reservation.id}
data={reservation.listing}
reservation={reservation}
actionId={reservation.id}
onAction={onCancel}
disabled={deletingId === reservation.id}
actionLabel="Cancel reservation"
currentUser={currentUser}
/>
))}
</div>
</Container>
);
}
export default ReservationsClient;
================================================
FILE: app/reservations/page.tsx
================================================
import ClientOnly from "@/components/ClientOnly";
import EmptyState from "@/components/EmptyState";
import React from "react";
import getCurrentUser from "../actions/getCurrentUser";
import getReservation from "../actions/getReservations";
import ReservationsClient from "./ReservationsClient";
type Props = {};
const ReservationsPage = async (props: Props) => {
const currentUser = await getCurrentUser();
if (!currentUser) {
return (
<ClientOnly>
<EmptyState title="Unauthorized" subtitle="Please login" />
</ClientOnly>
);
}
const reservations = await getReservation({
authorId: currentUser.id,
});
if (reservations.length === 0) {
return (
<ClientOnly>
<EmptyState
title="No Reservation found"
subtitle="Looks like you have no reservations on your properties."
/>
</ClientOnly>
);
}
return (
<ClientOnly>
<ReservationsClient
reservations={reservations}
currentUser={currentUser}
/>
</ClientOnly>
);
};
export default ReservationsPage;
================================================
FILE: app/trips/TripsClient.tsx
================================================
"use client";
import Container from "@/components/Container";
import Heading from "@/components/Heading";
import ListingCard from "@/components/listing/ListingCard";
import { SafeReservation, SafeUser } from "@/types";
import axios from "axios";
import { useRouter } from "next/navigation";
import React, { useCallback, useState } from "react";
import { toast } from "react-toastify";
type Props = {
reservations: SafeReservation[];
currentUser?: SafeUser | null;
};
function TripsClient({ reservations, currentUser }: Props) {
const router = useRouter();
const [deletingId, setDeletingId] = useState("");
const onCancel = useCallback(
(id: string) => {
setDeletingId(id);
axios
.delete(`/api/reservations/${id}`)
.then(() => {
toast.info("Reservation cancelled");
router.refresh();
})
.catch((error) => {
toast.error(error?.response?.data?.error);
})
.finally(() => {
setDeletingId("");
});
},
[router]
);
return (
<Container>
<Heading
title="Trips"
subtitle="Where you've been and where you're going"
/>
<div className="mt-10 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-5 gap-8">
{reservations.map((reservation) => (
<ListingCard
key={reservation.id}
data={reservation.listing}
reservation={reservation}
actionId={reservation.id}
onAction={onCancel}
disabled={deletingId === reservation.id}
actionLabel="Cancel reservation"
currentUser={currentUser}
/>
))}
</div>
</Container>
);
}
export default TripsClient;
================================================
FILE: app/trips/page.tsx
================================================
import ClientOnly from "@/components/ClientOnly";
import EmptyState from "@/components/EmptyState";
import React from "react";
import getCurrentUser from "../actions/getCurrentUser";
import getReservation from "../actions/getReservations";
import TripsClient from "./TripsClient";
type Props = {};
const TripsPage = async (props: Props) => {
const currentUser = await getCurrentUser();
if (!currentUser) {
return (
<ClientOnly>
<EmptyState title="Unauthorized" subtitle="Please login" />
</ClientOnly>
);
}
const reservations = await getReservation({
userId: currentUser.id,
});
if (reservations.length === 0) {
return (
<ClientOnly>
<EmptyState
title="No trips found"
subtitle="Looks like you havent reserved any trips."
/>
</ClientOnly>
);
}
return (
<ClientOnly>
<TripsClient reservations={reservations} currentUser={currentUser} />
</ClientOnly>
);
};
export default TripsPage;
================================================
FILE: components/Avatar.tsx
================================================
"use client";
import Image from "next/image";
import React from "react";
type Props = {
src: string | null | undefined;
userName?: string | null | undefined;
};
function Avatar({ src, userName }: Props) {
return (
<div>
{src ? (
<Image
className="rounded-full"
height="30"
width="30"
alt="hasImag"
src={src}
/>
) : userName ? (
<img
className="rounded-full h-[30px] w-[30px]"
alt="nameImage"
src={`https://ui-avatars.com/api/?name=${userName}`}
/>
) : (
<Image
className="rounded-full"
height="30"
width="30"
alt="noUser"
src="/assets/avatar.png"
/>
)}
</div>
);
}
export default Avatar;
================================================
FILE: components/Button.tsx
================================================
"use client";
import React from "react";
import { IconType } from "react-icons";
type Props = {
label: string;
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
disabled?: boolean;
outline?: boolean;
small?: boolean;
icon?: IconType;
isColor?: boolean;
};
function Button({
label,
onClick,
disabled,
outline,
small,
icon: Icon,
isColor,
}: Props) {
return (
<button
disabled={disabled}
onClick={onClick}
className={`relative disabled:opacity-70 disabled:cursor-not-allowed rounded-lg hover:opacity-80 transition w-full ${
outline ? "bg-white" : "bg-rose-500"
} ${outline ? "border-black" : "border-rose-500"} ${
outline ? "text-black" : "text-white"
} ${small ? "text-sm" : "text-md"} ${small ? "py-1" : "py-3"} ${
small ? "font-light" : "font-semibold"
} ${small ? "border-[1px]" : "border-2"}`}
>
{Icon && (
<Icon
size={24}
className={`absolute left-4 top-3 ${isColor && "text-blue-600"}`}
/>
)}
{label}
</button>
);
}
export default Button;
================================================
FILE: components/CategoryBox.tsx
================================================
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import qs from "query-string";
import React, { useCallback } from "react";
import { IconType } from "react-icons";
type Props = {
icon: IconType;
label: string;
selected?: boolean;
};
function CategoryBox({ icon: Icon, label, selected }: Props) {
const router = useRouter();
const params = useSearchParams();
const handleClick = useCallback(() => {
let currentQuery = {};
if (params) {
currentQuery = qs.parse(params.toString());
}
const updatedQuery: any = {
...currentQuery,
category: label,
};
if (params?.get("category") === label) {
delete updatedQuery.category;
}
const url = qs.stringifyUrl(
{
url: "/",
query: updatedQuery,
},
{ skipNull: true }
);
router.push(url);
}, [label, params, router]);
return (
<div
onClick={handleClick}
className={`flex flex-col items-center justify-center gap-2 p-3 border-b-2 hover:text-neutral-800 transition cursor-pointer ${
selected ? "border-b-neutral-800" : "border-transparent"
} ${selected ? "text-neutral-800" : "text-neutral-500"}`}
>
<Icon size={26} />
<div className="font-medium text-xs">{label}</div>
</div>
);
}
export default CategoryBox;
================================================
FILE: components/ClientOnly.tsx
================================================
"use client";
import { motion } from "framer-motion";
import React, { useEffect, useState } from "react";
type Props = {
children: React.ReactNode;
};
function ClientOnly({ children }: Props) {
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => {
setHasMounted(true);
}, []);
if (!hasMounted) {
return null;
}
return (
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
>
{children}
</motion.div>
);
}
export default ClientOnly;
================================================
FILE: components/Container.tsx
================================================
"use client";
import React from "react";
type Props = {
children: React.ReactNode;
};
function Container({ children }: Props) {
return (
<div className="max-w-[2520px] mx-auto xl:px-20 md:px-10 sm:px-2 px-4">
{children}
</div>
);
}
export default Container;
================================================
FILE: components/EmptyState.tsx
================================================
"use client";
import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
import React from "react";
import Button from "./Button";
import Heading from "./Heading";
type Props = {
title?: string;
subtitle?: string;
showReset?: boolean;
};
function EmptyState({
title = "No exact matches",
subtitle = "Try changing or removing some of your filters.",
showReset,
}: Props) {
const router = useRouter();
return (
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="h-[60vh] flex flex-col gap-2 justify-center items-center"
>
<Heading center title={title} subtitle={subtitle} />
<div className="w-48 mt-4">
{showReset && (
<Button
outline
label="Remove all filters"
onClick={() => router.push("/")}
/>
)}
</div>
</motion.div>
);
}
export default EmptyState;
================================================
FILE: components/Footer.tsx
================================================
"use client";
import { motion } from "framer-motion";
import React, { useEffect, useState } from "react";
import ClientOnly from "./ClientOnly";
import FooterColumn from "@/components/FooterColumn";
type Props = {};
function Footer({}: Props) {
const [country, setCountry] = useState("United States");
const itemData = [
["ABOUT", "Newsroom", "Learn about new features", "Letter from our founders", "Careers", "Investors"],
["Support", "Help Center", "AirCover", "Cancellation options", "Safety information", "Report a neighborhood concern"],
["Community", "Newsroom", "Learn about new features", "Letter from our founders", "Careers", "Investors"],
["Hosting","Try hosting","AirCover for Hosts","Explore hosting resources","Safety information","How to host responsibly"],
];
useEffect(() => {
fetch(
`https://extreme-ip-lookup.com/json/?key=${process.env.NEXT_PUBLIC_LOOKUP_KEY}`
)
.then((res) => res.json())
.then((data) => setCountry(data.country));
}, []);
const footerColumns = itemData.map((item, index) => (
<FooterColumn index={index} data={item} />
))
return (
<ClientOnly>
<div className="grid grid-cols-1 md:grid-cols-4 gap-y-10 px-32 py-14 bg-gray-100 text-gray-600">
{footerColumns}
<p className="text-sm">{country}</p>
</div>
</ClientOnly>
);
}
export default Footer;
================================================
FILE: components/FooterColumn.tsx
================================================
"user client"
import { motion } from "framer-motion";
type Props = {
index: number;
data: Array<string>;
};
function FooterColumn({ index, data }: Props) {
const columnItems = data.map((item, index) =>
index === 0
? <h5 className="font-bold">{item}</h5>
: <p>{item}</p>);
return (<motion.div
initial={{
x: index % 2 === 0 ? -200 : 200,
opacity: 0,
}}
transition={{ duration: 1 }}
whileInView={{ opacity: 1, x: 0 }}
className="space-y-4 text-xs text-gray-800"
>
{columnItems}
</motion.div>
);
}
export default FooterColumn;
================================================
FILE: components/Heading.tsx
================================================
"use client";
import React from "react";
type Props = {
title: string;
subtitle?: string;
center?: boolean;
};
function Heading({ title, subtitle, center }: Props) {
return (
<div className={center ? "text-center" : "text-start"}>
<div className="text-2xl font-bold">{title}</div>
<div className="font-light text-neutral-500 mt-2">{subtitle}</div>
</div>
);
}
export default Heading;
================================================
FILE: components/HeartButton.tsx
================================================
"use client";
import useFavorite from "@/hook/useFavorite";
import { SafeUser } from "@/types";
import React from "react";
import { AiFillHeart, AiOutlineHeart } from "react-icons/ai";
type Props = {
listingId: string;
currentUser?: SafeUser | null;
};
function HeartButton({ listingId, currentUser }: Props) {
const { hasFavorite, toggleFavorite } = useFavorite({
listingId,
currentUser,
});
return (
<div
onClick={toggleFavorite}
className=" relative hover:opacity-80 transition cursor-pointer"
>
<AiOutlineHeart
size={28}
className="fill-white absolute -top-[2px] -right-[2px]"
/>
<AiFillHeart
size={24}
className={hasFavorite ? "fill-rose-500" : "fill-neutral-500/70"}
/>
</div>
);
}
export default HeartButton;
================================================
FILE: components/ListingClient.tsx
================================================
"use client";
import useLoginModel from "@/hook/useLoginModal";
import { SafeReservation, SafeUser, safeListing } from "@/types";
import axios from "axios";
import { differenceInCalendarDays, eachDayOfInterval } from "date-fns";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Range } from "react-date-range";
import { toast } from "react-toastify";
import Container from "./Container";
import ListingHead from "./listing/ListingHead";
import ListingInfo from "./listing/ListingInfo";
import ListingReservation from "./listing/ListingReservation";
import { categories } from "./navbar/Categories";
const initialDateRange = {
startDate: new Date(),
endDate: new Date(),
key: "selection",
};
type Props = {
reservations?: SafeReservation[];
listing: safeListing & {
user: SafeUser;
};
currentUser?: SafeUser | null;
};
function ListingClient({ reservations = [], listing, currentUser }: Props) {
const router = useRouter();
const loginModal = useLoginModel();
const disableDates = useMemo(() => {
let dates: Date[] = [];
reservations.forEach((reservation) => {
const range = eachDayOfInterval({
start: new Date(reservation.startDate),
end: new Date(reservation.endDate),
});
dates = [...dates, ...range];
});
return dates;
}, [reservations]);
const [isLoading, setIsLoading] = useState(false);
const [totalPrice, setTotalPrice] = useState(listing.price);
const [dateRange, setDateRange] = useState<Range>(initialDateRange);
const onCreateReservation = useCallback(() => {
if (!currentUser) {
return loginModal.onOpen();
}
setIsLoading(true);
axios
.post("/api/reservations", {
totalPrice,
startDate: dateRange.startDate,
endDate: dateRange.endDate,
listingId: listing?.id,
})
.then(() => {
toast.success("Success!");
setDateRange(initialDateRange);
router.push("/trips");
})
.catch(() => {
toast.error("Something Went Wrong");
})
.finally(() => {
setIsLoading(false);
});
}, [totalPrice, dateRange, listing?.id, router, currentUser, loginModal]);
useEffect(() => {
if (dateRange.startDate && dateRange.endDate) {
const dayCount = differenceInCalendarDays(
dateRange.endDate,
dateRange.startDate
);
if (dayCount && listing.price) {
setTotalPrice(dayCount * listing.price);
} else {
setTotalPrice(listing.price);
}
}
}, [dateRange, listing.price]);
const category = useMemo(() => {
return categories.find((item) => item.label === listing.category);
}, [listing.category]);
return (
<Container>
<div className="max-w-screen-lg mx-auto">
<div className="flex flex-col gap-6">
<ListingHead
title={listing.title}
imageSrc={listing.imageSrc}
locationValue={listing.locationValue}
id={listing.id}
currentUser={currentUser}
/>
<div className="grid grid-cols-1 md:grid-cols-7 md:gap-10 mt-6">
<ListingInfo
user={listing.user}
category={category}
description={listing.description}
roomCount={listing.roomCount}
guestCount={listing.guestCount}
bathroomCount={listing.bathroomCount}
locationValue={listing.locationValue}
/>
<div className="order-first mb-10 md:order-last md:col-span-3">
<ListingReservation
price={listing.price}
totalPrice={totalPrice}
onChangeDate={(value) => setDateRange(value)}
dateRange={dateRange}
onSubmit={onCreateReservation}
disabled={isLoading}
disabledDates={disableDates}
/>
</div>
</div>
</div>
</div>
</Container>
);
}
export default ListingClient;
================================================
FILE: components/Loader.tsx
================================================
"use client";
import { motion } from "framer-motion";
import React from "react";
type Props = {};
function Loader({}: Props) {
return (
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="h-[70vh] flex flex-col justify-center items-center overflow-hidden"
>
<div className="px-4 py-12">
<div className="rounded relative bg-white py-12">
<div className="rounded-full bg-indigo-100 w-[177px] h-[177px] relative flex justify-center items-center mx-auto animate-spin">
<svg
className="absolute top-0 0 left-0"
width={177}
height={177}
viewBox="0 0 177 177"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M169.667 88.5C173.717 88.5 177.032 85.2113 176.696 81.1755C175.457 66.2542 170.448 51.8477 162.085 39.332C152.361 24.7783 138.539 13.435 122.367 6.73666C106.196 0.0383073 88.4018 -1.71429 71.2345 1.7005C54.0672 5.11529 38.298 13.5441 25.9211 25.921C13.5441 38.298 5.1153 54.0672 1.7005 71.2345C-1.71429 88.4018 0.0383044 106.196 6.73666 122.367C13.435 138.539 24.7783 152.361 39.332 162.085C51.8477 170.448 66.2542 175.457 81.1755 176.696C85.2113 177.032 88.5 173.717 88.5 169.667V169.667C88.5 165.618 85.2089 162.373 81.1792 161.971C69.1624 160.774 57.5826 156.642 47.4795 149.891C35.3374 141.778 25.8738 130.247 20.2855 116.755C14.6971 103.264 13.2349 88.4181 16.0838 74.0955C18.9328 59.773 25.9649 46.6168 36.2909 36.2908C46.6169 25.9649 59.773 18.9328 74.0955 16.0838C88.4181 13.2349 103.264 14.6971 116.755 20.2855C130.247 25.8739 141.778 35.3375 149.891 47.4795C156.642 57.5826 160.774 69.1624 161.971 81.1793C162.373 85.209 165.618 88.5 169.667 88.5V88.5Z"
fill="#FF5A5F"
/>
</svg>
<div className="div rounded-full bg-white w-[150px] h-[150px]" />
</div>
<p className="mt-6 font-medium text-gray-800 text-center animate-bounce text-xl">
Loading ...
</p>
</div>
</div>
</motion.div>
);
}
export default Loader;
================================================
FILE: components/Map.tsx
================================================
"use client";
import L from "leaflet";
import React from "react";
import { MapContainer, Marker, Popup, TileLayer } from "react-leaflet";
import markerIcon2x from "leaflet/dist/images/marker-icon-2x.png";
import markerIcon from "leaflet/dist/images/marker-icon.png";
import markerShadow from "leaflet/dist/images/marker-shadow.png";
import "leaflet/dist/leaflet.css";
import Flag from "react-world-flags";
// @ts-ignore
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconUrl: markerIcon.src,
iconRetinaUrl: markerIcon2x.src,
shadowUrl: markerShadow.src,
});
type Props = {
center?: number[];
locationValue?: string;
};
function Map({ center, locationValue }: Props) {
return (
<MapContainer
center={(center as L.LatLngExpression) || [51, -0.09]}
zoom={center ? 4 : 2}
scrollWheelZoom={false}
className="h-[35vh] rounded-lg"
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
{locationValue ? (
<>
{center && (
<Marker position={center as L.LatLngExpression}>
<Popup>
<div className="flex justify-center items-center animate-bounce">
<Flag code={locationValue} className="w-10" />
</div>
</Popup>
</Marker>
)}
</>
) : (
<>{center && <Marker position={center as L.LatLngExpression} />}</>
)}
</MapContainer>
);
}
export default Map;
================================================
FILE: components/Offers.tsx
================================================
"use client";
import { motion } from "framer-motion";
import { AiOutlineCar, AiOutlineWifi } from "react-icons/ai";
import { BiCctv } from "react-icons/bi";
import { BsFire } from "react-icons/bs";
import { FaFireExtinguisher } from "react-icons/fa";
import { GiButterflyFlower } from "react-icons/gi";
import { GrWorkshop } from "react-icons/gr";
import { MdOutlineBathtub, MdOutlineCoffeeMaker } from "react-icons/md";
import { RiSafeLine } from "react-icons/ri";
const offersRowOne = [
{
label: "Garden view",
icon: GiButterflyFlower,
},
{
label: "Hot water",
icon: BsFire,
},
{
label: "Wifi",
icon: AiOutlineWifi,
},
{
label: "Coffee",
icon: MdOutlineCoffeeMaker,
},
{
label: "Security cameras on property",
icon: BiCctv,
},
];
const offersRowTwo = [
{
label: "Bathtub",
icon: MdOutlineBathtub,
},
{
label: "Dedicated workspace",
icon: GrWorkshop,
},
{
label: "Safe",
icon: RiSafeLine,
},
{
label: "Free parking on premises",
icon: AiOutlineCar,
},
{
label: "Fire extinguisher",
icon: FaFireExtinguisher,
},
];
type Props = {};
function Offers({}: Props) {
return (
<div>
<p className="text-xl font-semibold">What this place offers</p>
<div className="flex justify-start space-x-12 pt-6">
<div className="flex flex-col gap-2">
{offersRowOne.map((item, index) => (
<motion.div
initial={{
x: -200,
opacity: 0,
}}
transition={{ duration: 1 }}
whileInView={{ opacity: 1, x: 0 }}
key={index}
className="flex justify-start items-center text-center gap-4 my-1 cursor-pointer"
>
<item.icon size={25} className="text-gray-700" />
<p className="text-neutral-500">{item.label}</p>
</motion.div>
))}
</div>
{/* another row */}
<div className="flex flex-col gap-2">
{offersRowTwo.map((item, index) => (
<motion.div
initial={{
x: 200,
opacity: 0,
}}
transition={{ duration: 1 }}
whileInView={{ opacity: 1, x: 0 }}
key={index}
className="flex justify-start items-center text-center gap-4 my-1 cursor-pointer"
>
<item.icon size={25} className="text-gray-700" />
<p className="text-neutral-500">{item.label}</p>
</motion.div>
))}
</div>
</div>
</div>
);
}
export default Offers;
================================================
FILE: components/Sleep.tsx
================================================
"use client";
import { motion } from "framer-motion";
import { BiBed } from "react-icons/bi";
import { IoBedOutline } from "react-icons/io5";
type Props = {};
function Sleep({}: Props) {
return (
<div>
<p className="text-xl font-semibold">{`Where you'll sleep`}</p>
<div className="flex justify-between pt-6">
<motion.div
initial={{
x: 200,
opacity: 0,
}}
transition={{ duration: 1 }}
whileInView={{ opacity: 1, x: 0 }}
className="border border-black rounded-md cursor-pointer"
>
<div className="flex flex-col justify-start items-start px-6 py-6 gap-1 text-center">
<IoBedOutline size={25} />
<p className="text-lg text-black font-medium">Bedroom 1</p>
<p className="text-sm text-neutral-500">1 king bed</p>
</div>
</motion.div>
<motion.div
initial={{
x: -200,
opacity: 0,
}}
transition={{ duration: 1 }}
whileInView={{ opacity: 1, x: 0 }}
className="border border-black rounded-md cursor-pointer"
>
<div className="flex flex-col justify-start items-start px-6 py-6 gap-1 text-center">
<IoBedOutline size={25} />
<p className="text-lg text-black font-medium">Bedroom 2</p>
<p className="text-sm text-neutral-500">1 queen bed</p>
</div>
</motion.div>
<motion.div
initial={{
x: 200,
opacity: 0,
}}
transition={{ duration: 1 }}
whileInView={{ opacity: 1, x: 0 }}
className="border border-black rounded-md cursor-pointer"
>
<div className="flex flex-col justify-start items-start px-6 py-6 gap-1 text-center">
<div className="flex justify-between gap-2">
<IoBedOutline size={25} />
<BiBed size={25} />
</div>
<p className="text-lg text-black font-medium">Bedroom 3</p>
<p className="text-sm text-neutral-500">
1 queen bed, 1 single bed
</p>
</div>
</motion.div>
</div>
</div>
);
}
export default Sleep;
================================================
FILE: components/ToastContainerBar.tsx
================================================
"use client";
import React from "react";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
type Props = {};
function ToastContainerBar({}: Props) {
return (
<>
<ToastContainer
position="bottom-left"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
pauseOnFocusLoss
pauseOnHover
theme="colored"
/>
</>
);
}
export default ToastContainerBar;
================================================
FILE: components/inputs/Calendar.tsx
================================================
"use client";
import React from "react";
import { DateRange, Range, RangeKeyDict } from "react-date-range";
import "react-date-range/dist/styles.css";
import "react-date-range/dist/theme/default.css";
type Props = {
value: Range;
onChange: (value: RangeKeyDict) => void;
disabledDates?: Date[];
};
function Calendar({ value, onChange, disabledDates }: Props) {
return (
<DateRange
rangeColors={["#262626"]}
ranges={[value]}
date={new Date()}
onChange={onChange}
direction="vertical"
showDateDisplay={false}
minDate={new Date()}
disabledDates={disabledDates}
/>
);
}
export default Calendar;
================================================
FILE: components/inputs/CategoryInput.tsx
================================================
"use client";
import React from "react";
import { IconType } from "react-icons";
type Props = {
icon: IconType;
label: string;
selected?: boolean;
onClick: (value: string) => void;
};
function CategoryInput({ icon: Icon, label, selected, onClick }: Props) {
return (
<div
onClick={() => onClick(label)}
className={` rounded-xl border-2 p-4 flex flex-col gap-3 hover:border-black transition cursor-pointer ${
selected ? "border-black" : "border-neutral-200"
}`}
>
<Icon size={30} />
<div className="font-semibold">{label}</div>
</div>
);
}
export default CategoryInput;
================================================
FILE: components/inputs/Counter.tsx
================================================
"use client";
import React, { useCallback } from "react";
import { AiOutlineMinus, AiOutlinePlus } from "react-icons/ai";
type Props = {
title: string;
subtitle: string;
value: number;
onChange: (value: number) => void;
};
function Counter({ title, subtitle, value, onChange }: Props) {
const onAdd = useCallback(() => {
onChange(value + 1);
}, [onChange, value]);
const onReduce = useCallback(() => {
if (value === 1) {
return;
}
onChange(value - 1);
}, [value, onChange]);
return (
<div className="flex flex-row items-center justify-between">
<div className="flex flex-col">
<div className="font-medium">{title}</div>
<div className="font-light text-gray-600">{subtitle}</div>
</div>
<div className="flex flex-row items-center gap-4">
<div
onClick={onReduce}
className=" w-10 h-10 rounded-full border-[1px] border-neutral-400 flex items-center justify-center text-neutral-600 cursor-pointer hover:opacity-80 transition"
>
<AiOutlineMinus />
</div>
<div className="font-light text-xl text-neutral-600">{value}</div>
<div
onClick={onAdd}
className="w-10 h-10 rounded-full border-[1px] border-neutral-400 flex items-center justify-center text-neutral-600 cursor-pointer hover:opacity-80 transition"
>
<AiOutlinePlus />
</div>
</div>
</div>
);
}
export default Counter;
================================================
FILE: components/inputs/CountrySelect.tsx
================================================
"use client";
import useCountries from "@/hook/useCountries";
import Select from "react-select";
import Flag from "react-world-flags";
export type CountrySelectValue = {
flag: string;
label: string;
latlng: number[];
region: string;
value: string;
};
type Props = {
value?: CountrySelectValue;
onChange: (value: CountrySelectValue) => void;
};
function CountrySelect({ value, onChange }: Props) {
const { getAll } = useCountries();
return (
<div>
<Select
placeholder="Anywhere"
isClearable
options={getAll()}
value={value}
onChange={(value) => onChange(value as CountrySelectValue)}
formatOptionLabel={(option: any) => (
<div className="flex flex-row items-center gap-3">
<Flag code={option.value} className="w-5" />
<div>
{option.label},
<span className="text-neutral-500 ml-1">{option.region}</span>
</div>
</div>
)}
classNames={{
control: () => "p-3 border-2",
input: () => "text-lg",
option: () => "text-lg",
}}
theme={(theme) => ({
...theme,
borderRadius: 6,
colors: {
...theme.colors,
primary: "black",
primary25: "#ffe4e6",
},
})}
/>
</div>
);
}
export default CountrySelect;
================================================
FILE: components/inputs/ImageUpload.tsx
================================================
"use client";
import { CldUploadWidget } from "next-cloudinary";
import Image from "next/image";
import React, { useCallback } from "react";
import { TbPhotoPlus } from "react-icons/tb";
declare global {
var cloudinary: any;
}
type Props = {
onChange: (value: string) => void;
value: string;
};
function ImageUpload({ onChange, value }: Props) {
const handleCallback = useCallback(
(result: any) => {
onChange(result.info.secure_url);
},
[onchange]
);
return (
<CldUploadWidget
onUpload={handleCallback}
uploadPreset="cptcecyi"
options={{
maxFiles: 1,
}}
>
{({ open }) => {
return (
<div
onClick={() => open?.()}
className=" relative cursor-pointer hover:opacity-70 transition border-dashed border-2 p-20 border-neutral-300 flex flex-col justify-center items-center gap-4 text-neutral-600"
>
<TbPhotoPlus size={50} />
<div className="font-semibold text-lg">Click to upload</div>
{value && (
<div className=" absolute inset-0 w-full h-full">
<Image
alt="uploade"
fill
style={{ objectFit: "cover" }}
src={value}
/>
</div>
)}
</div>
);
}}
</CldUploadWidget>
);
}
export default ImageUpload;
================================================
FILE: components/inputs/Input.tsx
================================================
import React from "react";
import { FieldErrors, FieldValues, UseFormRegister } from "react-hook-form";
import { BiDollar } from "react-icons/bi";
type Props = {
id: string;
label: string;
type?: string;
disabled?: boolean;
formatPrice?: boolean;
required?: boolean;
register: UseFormRegister<FieldValues>;
errors: FieldErrors;
};
function Input({
id,
label,
type = "text",
disabled,
formatPrice,
register,
required,
errors,
}: Props) {
return (
<div className="w-full relative">
{formatPrice && (
<BiDollar
size={24}
className="
text-neutral-700
absolute
top-5
left-2
"
/>
)}
<input
id={id}
disabled={disabled}
{...register(id, { required })}
placeholder=" "
type={type}
className={`peer w-full p-4 pt-6 font-light bg-white border-2 rounded-md outline-none transition disabled:opacity-70 disabled:cursor-not-allowed ${
formatPrice ? "pl-9" : "pl-4"
} ${errors[id] ? "border-rose-500" : "border-neutral-300"} ${
errors[id] ? "focus:border-rose-500" : "focus:border-black"
}`}
/>
<label
className={`absolute text-md duration-150 transform -translate-y-3 top-5 z-10 origin-[0] ${
formatPrice ? "left-9" : "left-4"
} peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-4 ${
errors[id] ? "text-rose-500" : "text-zinc-400"
}`}
>
{label}
</label>
</div>
);
}
export default Input;
================================================
FILE: components/listing/ListingCard.tsx
================================================
"use client";
import useCountries from "@/hook/useCountries";
import { SafeReservation, SafeUser, safeListing } from "@/types";
import { format } from "date-fns";
import { motion } from "framer-motion";
import Image from "next/image";
import { useRouter } from "next/navigation";
import React, { useCallback, useMemo } from "react";
import Button from "../Button";
import HeartButton from "../HeartButton";
type Props = {
data: safeListing;
reservation?: SafeReservation;
onAction?: (id: string) => void;
disabled?: boolean;
actionLabel?: string;
actionId?: string;
currentUser?: SafeUser | null;
};
function ListingCard({
data,
reservation,
onAction,
disabled,
actionLabel,
actionId = "",
currentUser,
}: Props) {
const router = useRouter();
const { getByValue } = useCountries();
const location = getByValue(data.locationValue);
const handleCancel = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
if (disabled) return;
onAction?.(actionId);
},
[onAction, actionId, disabled]
);
const price = useMemo(() => {
if (reservation) {
return reservation.totalPrice;
}
return data.price;
}, [reservation, data.price]);
const reservationDate = useMemo(() => {
if (!reservation) {
return null;
}
const start = new Date(reservation.startDate);
const end = new Date(reservation.endDate);
return `${format(start, "PP")} - ${format(end, "PP")}`;
}, [reservation]);
return (
<motion.div
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.8,
delay: 0.5,
ease: [0, 0.71, 0.2, 1.01],
}}
onClick={() => router.push(`/listings/${data.id}`)}
className="col-span-1 cursor-pointer group"
>
<div className="flex flex-col gap-2 w-full">
<div className="aspect-square w-full relative overflow-hidden rounded-xl">
<Image
fill
className="object-cover h-full w-full group-hover:scale-110 transition"
src={data.imageSrc}
alt="listing"
/>
<div className="absolute top-3 right-3">
<HeartButton listingId={data.id} currentUser={currentUser} />
</div>
</div>
<div className="font-semibold text-lg">
{location?.region}, {location?.label}
</div>
<div className="font-light text-neutral-500">
{reservationDate || data.category}
</div>
<div className="flex flex-row items-center gap-">
<div className="flex gap-1 font-semibold">
${price} {!reservation && <div className="font-light"> Night</div>}
</div>
</div>
{onAction && actionLabel && (
<Button
disabled={disabled}
small
label={actionLabel}
onClick={handleCancel}
/>
)}
</div>
</motion.div>
);
}
export default ListingCard;
================================================
FILE: components/listing/ListingCategory.tsx
================================================
"use client";
import React from "react";
import { IconType } from "react-icons";
type Props = {
icon: IconType;
label: string;
description: string;
};
function ListingCategory({ icon: Icon, label, description }: Props) {
return (
<div className="flex flex-col gap-6">
<div className="flex flex-row items-center gap-4">
<Icon size={40} className="text-neutral-600" />
<div className="flex flex-col">
<p className="text-lg font-semibold">{label}</p>
<p className="text-neutral-500 font-light">{description}</p>
</div>
</div>
</div>
);
}
export default ListingCategory;
================================================
FILE: components/listing/ListingHead.tsx
================================================
"use client";
import useCountries from "@/hook/useCountries";
import { SafeUser } from "@/types";
import { motion } from "framer-motion";
import Image from "next/image";
import Heading from "../Heading";
import HeartButton from "../HeartButton";
type Props = {
title: string;
locationValue: string;
imageSrc: string;
id: string;
currentUser?: SafeUser | null;
};
function ListingHead({
title,
locationValue,
imageSrc,
id,
currentUser,
}: Props) {
const { getByValue } = useCountries();
const location = getByValue(locationValue);
return (
<>
<Heading
title={title}
subtitle={`${location?.region}, ${location?.label}`}
/>
<motion.div
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.8,
delay: 0.5,
ease: [0, 0.71, 0.2, 1.01],
}}
className="w-full h-[60vh] overflow-hidden rounded-xl relative"
>
<Image
src={imageSrc}
alt="image"
fill
className="object-cover w-full"
/>
<div className="absolute top-5 right-5">
<HeartButton listingId={id} currentUser={currentUser} />
</div>
</motion.div>
</>
);
}
export default ListingHead;
================================================
FILE: components/listing/ListingInfo.tsx
================================================
"use client";
import useCountries from "@/hook/useCountries";
import { SafeUser } from "@/types";
import dynamic from "next/dynamic";
import React from "react";
import { IconType } from "react-icons";
import Avatar from "../Avatar";
import ListingCategory from "./ListingCategory";
import Sleep from "../Sleep";
import Offers from "../Offers";
const Map = dynamic(() => import("../Map"), {
ssr: false,
});
type Props = {
user: SafeUser;
description: string;
guestCount: number;
roomCount: number;
bathroomCount: number;
category:
| {
icon: IconType;
label: string;
description: string;
}
| undefined;
locationValue: string;
};
function ListingInfo({
user,
description,
guestCount,
roomCount,
bathroomCount,
category,
locationValue,
}: Props) {
const { getByValue } = useCountries();
const coordinates = getByValue(locationValue)?.latlng;
return (
<div className="col-span-4 flex flex-col gap-8">
<div className="flex flex-col gap-2">
<div className=" text-xl font-semibold flex flex-row items-center gap-2">
<div>Hosted by {user?.name}</div>
<Avatar src={user?.image} userName={user?.name} />
</div>
<div className="flex flex-row items-center gap-4 font-light text-neutral-500">
<p>{guestCount} guests</p>
<p>{roomCount} rooms</p>
<p>{bathroomCount} bathrooms</p>
</div>
</div>
<hr />
{category && (
<ListingCategory
icon={category.icon}
label={category?.label}
description={category?.description}
/>
)}
<hr />
<div className="flex flex-col">
<p className="text-4xl font-bold text-[#FF5A5F]">
air<span className="text-black">cover</span>
</p>
<p className="text-neutral-500 pt-3">
Every booking includes free protection from Host cancellations,
listing inaccuracies, and other issues like trouble checking in.
</p>
<p className="text-black font-bold underline pt-3 cursor-pointer">
Learn more
</p>
</div>
<hr />
<p className="text-lg font-light text-neutral-500">{description}</p>
<hr />
<Sleep />
<hr />
<Offers />
<hr />
<p className="text-xl font-semibold">{`Where you’ll be`}</p>
<Map center={coordinates} locationValue={locationValue} />
</div>
);
}
export default ListingInfo;
================================================
FILE: components/listing/ListingReservation.tsx
================================================
"use client";
import React from "react";
import { Range } from "react-date-range";
import Calendar from "../inputs/Calendar";
import Button from "../Button";
type Props = {
price: number;
dateRange: Range;
totalPrice: number;
onChangeDate: (value: Range) => void;
onSubmit: () => void;
disabled?: boolean;
disabledDates: Date[];
};
function ListingReservation({
price,
dateRange,
totalPrice,
onChangeDate,
onSubmit,
disabled,
disabledDates,
}: Props) {
return (
<div className="bg-white rounded-xl border-[1px] border-neutral-200 overflow-hidden">
<div className="flex flex-row items-center gap-1 p-4">
<p className="flex gap-1 text-2xl font-semibold">
$ {price} <p className="font-light text-neutral-600">night</p>
</p>
</div>
<hr />
<Calendar
value={dateRange}
disabledDates={disabledDates}
onChange={(value) => onChangeDate(value.selection)}
/>
<hr />
<div className="p-4">
<Button disabled={disabled} label="Reserve" onClick={onSubmit} />
</div>
<hr />
<div className="p-4 flex flex-row items-center justify-between font-semibold text-lg">
<p>Total</p>
<p> $ {totalPrice}</p>
</div>
</div>
);
}
export default ListingReservation;
================================================
FILE: components/models/LoginModal.tsx
================================================
"use client";
import useLoginModel from "@/hook/useLoginModal";
import useRegisterModal from "@/hook/useRegisterModal";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useCallback, useState } from "react";
import { FieldValues, SubmitHandler, useForm } from "react-hook-form";
import { AiFillFacebook } from "react-icons/ai";
import { FcGoogle } from "react-icons/fc";
import { toast } from "react-toastify";
import Button from "../Button";
import Heading from "../Heading";
import Input from "../inputs/Input";
import Modal from "./Modal";
type Props = {};
function LoginModal({}: Props) {
const router = useRouter();
const registerModel = useRegisterModal();
const loginModel = useLoginModel();
const [isLoading, setIsLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FieldValues>({
defaultValues: {
email: "",
password: "",
},
});
const onSubmit: SubmitHandler<FieldValues> = (data) => {
setIsLoading(true);
signIn("credentials", {
...data,
redirect: false,
}).then((callback) => {
setIsLoading(false);
if (callback?.ok) {
toast.success("Login Successfully");
router.refresh();
loginModel.onClose();
} else if (callback?.error) {
toast.error("Something Went Wrong");
}
});
};
const toggle = useCallback(() => {
loginModel.onClose();
registerModel.onOpen();
}, [loginModel, registerModel]);
const bodyContent = (
<div className="flex flex-col gap-4">
<Heading title="Welcome Back" subtitle="Login to your Account!" center />
<Input
id="email"
label="Email Address"
disabled={isLoading}
register={register}
errors={errors}
required
/>
<Input
id="password"
label="Password"
disabled={isLoading}
register={register}
errors={errors}
required
/>
</div>
);
const footerContent = (
<div className="flex flex-col gap-4 mt-3">
<hr />
<Button
outline
label="Continue with Google"
icon={FcGoogle}
onClick={() => signIn("google")}
/>
<Button
outline
label="Continue with Facebook"
icon={AiFillFacebook}
onClick={() => signIn("facebook")}
isColor
/>
<div className="text-neutral-500 text-center mt-4 font-light">
<div>
{`Didn't have an Account?`}{" "}
<span
onClick={toggle}
className="text-neutral-800 cursor-pointer hover:underline"
>
Create an Account
</span>
</div>
</div>
</div>
);
return (
<Modal
disabled={isLoading}
isOpen={loginModel.isOpen}
title="Login"
actionLabel="Continue"
onClose={loginModel.onClose}
onSubmit={handleSubmit(onSubmit)}
body={bodyContent}
footer={footerContent}
/>
);
}
export default LoginModal;
================================================
FILE: components/models/Modal.tsx
================================================
"use client";
import React, { useCallback, useEffect, useState } from "react";
import { IoMdClose } from "react-icons/io";
import Button from "../Button";
type Props = {
isOpen?: boolean;
onClose: () => void;
onSubmit: () => void;
title?: string;
body?: React.ReactElement;
footer?: React.ReactElement;
actionLabel: string;
disabled?: boolean;
secondaryAction?: () => void;
secondaryActionLabel?: string;
};
function Modal({
isOpen,
onClose,
onSubmit,
title,
body,
actionLabel,
footer,
disabled,
secondaryAction,
secondaryActionLabel,
}: Props) {
const [showModal, setShowModal] = useState(isOpen);
useEffect(() => {
setShowModal(isOpen);
}, [isOpen]);
const handleClose = useCallback(() => {
if (disabled) {
return;
}
setShowModal(false);
setTimeout(() => {
onClose();
}, 300);
}, [disabled, onClose]);
const handleSubmit = useCallback(() => {
if (disabled) {
return;
}
onSubmit();
}, [onSubmit, disabled]);
const handleSecondAction = useCallback(() => {
if (disabled || !secondaryAction) {
return;
}
secondaryAction();
}, [disabled, secondaryAction]);
if (!isOpen) {
return null;
}
return (
<>
<div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none bg-neutral-800/70">
<div className="relative w-full md:w-4/6 lg:w-3/6 xl:w-2/5 my-6 mx-auto h-full lg:h-auto md:h-auto">
<div
className={`translate duration-300 h-full ${
showModal ? "translate-y-0" : "translate-y-full"
} ${showModal ? "opacity-100" : "opacity-0"}`}
>
<div className="translate h-full lg:h-auto md:h-auto border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
<div className="flex items-center p-6 rounded-t justify-center relative border-b-[1px]">
<button
className="p-1 border-0 hover:opacity-70 transition absolute left-9"
onClick={handleClose}
>
<IoMdClose size={18} />
</button>
<div className="text-lg font-semibold">{title}</div>
</div>
<div className="relative p-6 flex-auto">{body}</div>
<div className="flex flex-col gap-2 p-6">
<div className="flex flex-row items-center gap-4 w-full">
{secondaryAction && secondaryActionLabel && (
<Button
outline
disabled={disabled}
label={secondaryActionLabel}
onClick={handleSecondAction}
/>
)}
<Button
disabled={disabled}
label={actionLabel}
onClick={handleSubmit}
/>
</div>
{footer}
</div>
</div>
</div>
</div>
</div>
</>
);
}
export default Modal;
================================================
FILE: components/models/RegisterModal.tsx
================================================
"use client";
import useLoginModel from "@/hook/useLoginModal";
import useRegisterModal from "@/hook/useRegisterModal";
import axios from "axios";
import { useCallback, useState } from "react";
import { FieldValues, SubmitHandler, useForm } from "react-hook-form";
import { AiFillFacebook } from "react-icons/ai";
import { FcGoogle } from "react-icons/fc";
import { toast } from "react-toastify";
import { signIn } from "next-auth/react";
import Button from "../Button";
import Heading from "../Heading";
import Input from "../inputs/Input";
import Modal from "./Modal";
type Props = {};
function RegisterModal({}: Props) {
const registerModel = useRegisterModal();
const loginModel = useLoginModel();
const [isLoading, setIsLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FieldValues>({
defaultValues: {
name: "",
email: "",
password: "",
},
});
const onSubmit: SubmitHandler<FieldValues> = (data) => {
setIsLoading(true);
axios
.post("/api/register", data)
.then(() => {
toast.success("Success!");
loginModel.onOpen();
registerModel.onClose();
})
.catch((err: any) => toast.error("Something Went Wrong"))
.finally(() => {
setIsLoading(false);
toast.success("Register Successfully");
});
};
const toggle = useCallback(() => {
loginModel.onOpen();
registerModel.onClose();
}, [loginModel, registerModel]);
const bodyContent = (
<div className="flex flex-col gap-4">
<Heading
title="Welcome to Airbnb-Clone"
subtitle="Create an Account!"
center
/>
<Input
id="email"
label="Email Address"
disabled={isLoading}
register={register}
errors={errors}
required
/>
<Input
id="name"
label="User Name"
disabled={isLoading}
register={register}
errors={errors}
required
/>
<Input
id="password"
label="Password"
disabled={isLoading}
register={register}
errors={errors}
required
/>
</div>
);
const footerContent = (
<div className="flex flex-col gap-4 mt-3">
<hr />
<Button
outline
label="Continue with Google"
icon={FcGoogle}
onClick={() => signIn("google")}
/>
<Button
outline
label="Continue with Facebook"
icon={AiFillFacebook}
onClick={() => signIn("facebook")}
isColor
/>
<div className="text-neutral-500 text-center mt-4 font-light">
<div>
Already have an account?{" "}
<span
onClick={toggle}
className="text-neutral-800 cursor-pointer hover:underline"
>
Log in
</span>
</div>
</div>
</div>
);
return (
<Modal
disabled={isLoading}
isOpen={registerModel.isOpen}
title="Register"
actionLabel="Continue"
onClose={registerModel.onClose}
onSubmit={handleSubmit(onSubmit)}
body={bodyContent}
footer={footerContent}
/>
);
}
export default RegisterModal;
================================================
FILE: components/models/RentModal.tsx
================================================
"use client";
import useRentModal from "@/hook/useRentModal";
import axios from "axios";
import dynamic from "next/dynamic";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import { FieldValues, SubmitHandler, useForm } from "react-hook-form";
import { toast } from "react-toastify";
import Heading from "../Heading";
import CategoryInput from "../inputs/CategoryInput";
import Counter from "../inputs/Counter";
import CountrySelect from "../inputs/CountrySelect";
import ImageUpload from "../inputs/ImageUpload";
import Input from "../inputs/Input";
import { categories } from "../navbar/Categories";
import Modal from "./Modal";
type Props = {};
enum STEPS {
CATEGORY = 0,
LOCATION = 1,
INFO = 2,
IMAGES = 3,
DESCRIPTION = 4,
PRICE = 5,
}
function RentModal({}: Props) {
const router = useRouter();
const rentModel = useRentModal();
const [step, setStep] = useState(STEPS.CATEGORY);
const [isLoading, setIsLoading] = useState(false);
const {
register,
handleSubmit,
setValue,
watch,
formState: { errors },
reset,
} = useForm<FieldValues>({
defaultValues: {
category: "",
location: null,
guestCount: 1,
roomCount: 1,
bathroomCount: 1,
imageSrc: "",
price: 1,
title: "",
description: "",
},
});
const category = watch("category");
const location = watch("location");
const guestCount = watch("guestCount");
const roomCount = watch("roomCount");
const bathroomCount = watch("bathroomCount");
const imageSrc = watch("imageSrc");
const Map = useMemo(
() =>
dynamic(() => import("../Map"), {
ssr: false,
}),
[location]
);
const setCustomValue = (id: string, value: any) => {
setValue(id, value, {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
});
};
const onBack = () => {
setStep((value) => value - 1);
};
const onNext = () => {
setStep((value) => value + 1);
};
const onSubmit: SubmitHandler<FieldValues> = (data) => {
if (step !== STEPS.PRICE) {
return onNext();
}
setIsLoading(true);
axios
.post("/api/listings", data)
.then(() => {
toast.success("Listing Created!");
router.refresh();
reset();
setStep(STEPS.CATEGORY);
rentModel.onClose();
})
.catch(() => {
toast.error("Something Went Wrong");
})
.finally(() => {
setIsLoading(false);
});
};
const actionLabel = useMemo(() => {
if (step === STEPS.PRICE) {
return "Create";
}
return "Next";
}, [step]);
const secondActionLabel = useMemo(() => {
if (step === STEPS.CATEGORY) {
return undefined;
}
return "Back";
}, [step]);
let bodyContent = (
<div className="flex flex-col gap-8">
<Heading
title="Which of these best describes your place?"
subtitle="Pick a category"
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-[50vh] overflow-y-auto scrollbar-thin scrollbar-thumb-[#FF5A5F]">
{categories.map((item, index) => (
<div key={index} className="col-span-1">
<CategoryInput
onClick={(category) => setCustomValue("category", category)}
selected={category === item.label}
label={item.label}
icon={item.icon}
/>
</div>
))}
</div>
</div>
);
if (step === STEPS.LOCATION) {
bodyContent = (
<div className="flex flex-col gap-8">
<Heading
title="Where is your place located?"
subtitle="Help guests find you!"
/>
<CountrySelect
value={location}
onChange={(value) => setCustomValue("location", value)}
/>
<Map center={location?.latlng} />
</div>
);
}
if (step === STEPS.INFO) {
bodyContent = (
<div className="flex flex-col gap-8">
<Heading
title="Share some basics about your place"
subtitle="What amenities do you have?"
/>
<Counter
title="Guests"
subtitle="How many guest do you allow?"
value={guestCount}
onChange={(value) => setCustomValue("guestCount", value)}
/>
<hr />
<Counter
title="Rooms"
subtitle="How many rooms do you have?"
value={roomCount}
onChange={(value) => setCustomValue("roomCount", value)}
/>
<hr />
<Counter
title="Bathrooms"
subtitle="How many Bathrooms do you have?"
value={bathroomCount}
onChange={(value) => setCustomValue("bathroomCount", value)}
/>
</div>
);
}
if (step === STEPS.IMAGES) {
bodyContent = (
<div className="flex flex-col gap-8">
<Heading
title="Add a photo of your place"
subtitle="Show guests what your place looks like!"
/>
<ImageUpload
onChange={(value) => setCustomValue("imageSrc", value)}
value={imageSrc}
/>
</div>
);
}
if (step === STEPS.DESCRIPTION) {
bodyContent = (
<div className="flex flex-col gap-8">
<Heading
title="Now, set your price"
subtitle="How much do you charge per night?"
/>
<Input
id="title"
label="Title"
disabled={isLoading}
register={register}
errors={errors}
required
/>
<hr />
<Input
id="description"
label="Description"
disabled={isLoading}
register={register}
errors={errors}
required
/>
</div>
);
}
if (step == STEPS.PRICE) {
bodyContent = (
<div className="flex flex-col gap-8">
<Heading
title="Now, set your price"
subtitle="How much do you charge per night?"
/>
<Input
id="price"
label="Price"
formatPrice
type="number"
disabled={isLoading}
register={register}
errors={errors}
required
/>
</div>
);
}
return (
<Modal
disabled={isLoading}
isOpen={rentModel.isOpen}
title="Airbnb your home!"
actionLabel={actionLabel}
onSubmit={handleSubmit(onSubmit)}
secondaryActionLabel={secondActionLabel}
secondaryAction={step === STEPS.CATEGORY ? undefined : onBack}
onClose={rentModel.onClose}
body={bodyContent}
/>
);
}
export default RentModal;
================================================
FILE: components/models/SearchModal.tsx
================================================
"use client";
import useSearchModal from "@/hook/useSearchModal";
import { formatISO } from "date-fns";
import dynamic from "next/dynamic";
import { useRouter, useSearchParams } from "next/navigation";
import qs from "query-string";
import { useCallback, useMemo, useState } from "react";
import { Range } from "react-date-range";
import Heading from "../Heading";
import Calendar from "../inputs/Calendar";
import Counter from "../inputs/Counter";
import CountrySelect, { CountrySelectValue } from "../inputs/CountrySelect";
import Modal from "./Modal";
enum STEPS {
LOCATION = 0,
DATE = 1,
INFO = 2,
}
type Props = {};
function SearchModal({}: Props) {
const router = useRouter();
const params = useSearchParams();
const searchModel = useSearchModal();
const [location, setLocation] = useState<CountrySelectValue>();
const [step, setStep] = useState(STEPS.LOCATION);
const [guestCount, setGuestCount] = useState(1);
const [roomCount, setRoomCount] = useState(1);
const [bathroomCount, setBathroomCount] = useState(1);
const [dateRange, setDateRange] = useState<Range>({
startDate: new Date(),
endDate: new Date(),
key: "selection",
});
const Map = useMemo(
() =>
dynamic(() => import("../Map"), {
ssr: false,
}),
[location]
);
const onBack = () => {
setStep((value) => value - 1);
};
const onNext = () => {
setStep((value) => value + 1);
};
const onSubmit = useCallback(async () => {
if (step !== STEPS.INFO) {
return onNext();
}
let currentQuery = {};
if (params) {
currentQuery = qs.parse(params.toString());
}
const updatedQuery: any = {
...currentQuery,
locationValue: location?.value,
guestCount,
roomCount,
bathroomCount,
};
if (dateRange.startDate) {
updatedQuery.startDate = formatISO(dateRange.startDate);
}
if (dateRange.endDate) {
updatedQuery.endDate = formatISO(dateRange.endDate);
}
const url = qs.stringifyUrl(
{
url: "/",
query: updatedQuery,
},
{ skipNull: true }
);
setStep(STEPS.LOCATION);
searchModel.onClose();
router.push(url);
}, [
step,
searchModel,
location,
router,
guestCount,
roomCount,
bathroomCount,
dateRange,
onNext,
params,
]);
const actionLabel = useMemo(() => {
if (step === STEPS.INFO) {
return "Search";
}
return "Next";
}, [step]);
const secondActionLabel = useMemo(() => {
if (step === STEPS.LOCATION) {
return undefined;
}
return "Back";
}, [step]);
let bodyContent = (
<div className="flex flex-col gap-8">
<Heading
title="Where do you wanna go?"
subtitle="Find the perfect location!"
/>
<CountrySelect
value={location}
onChange={(value) => setLocation(value as CountrySelectValue)}
/>
<hr />
<Map center={location?.latlng} />
</div>
);
if (step === STEPS.DATE) {
bodyContent = (
<div className="flex flex-col gap-8">
<Heading
title="When do you plan to go?"
subtitle="Make sure everyone is free!"
/>
<Calendar
onChange={(value) => setDateRange(value.selection)}
value={dateRange}
/>
</div>
);
}
if (step === STEPS.INFO) {
bodyContent = (
<div className="flex flex-col gap-8">
<Heading title="More information" subtitle="Find your perfect place!" />
<Counter
onChange={(value) => setGuestCount(value)}
value={guestCount}
title="Guests"
subtitle="How many guests are coming?"
/>
<hr />
<Counter
onChange={(value) => setRoomCount(value)}
value={roomCount}
title="Rooms"
subtitle="How many rooms do you need?"
/>
<hr />
<Counter
onChange={(value) => {
setBathroomCount(value);
}}
value={bathroomCount}
title="Bathrooms"
subtitle="How many bahtrooms do you need?"
/>
</div>
);
}
return (
<Modal
isOpen={searchModel.isOpen}
onClose={searchModel.onClose}
onSubmit={onSubmit}
secondaryAction={step === STEPS.LOCATION ? undefined : onBack}
secondaryActionLabel={secondActionLabel}
title="Filters"
actionLabel="Search"
body={bodyContent}
/>
);
}
export default SearchModal;
================================================
FILE: components/navbar/Categories.tsx
================================================
"use client";
import { usePathname, useSearchParams } from "next/navigation";
import { BsSnow } from "react-icons/bs";
import { FaSkiing } from "react-icons/fa";
import {
GiBarn,
GiBoatFishing,
GiCactus,
GiCastle,
GiCaveEntrance,
GiForestCamp,
GiIsland,
GiWindmill,
} from "react-icons/gi";
import { IoDiamond } from "react-icons/io5";
import { MdOutlineVilla } from "react-icons/md";
import { TbBeach, TbMountain, TbPool } from "react-icons/tb";
import CategoryBox from "../CategoryBox";
import Container from "../Container";
export const categories = [
{
label: "Beach",
icon: TbBeach,
description: "This property is close to the beach!",
},
{
label: "Windmills",
icon: GiWindmill,
description: "This property is has windmills!",
},
{
label: "Modern",
icon: MdOutlineVilla,
description: "This property is modern!",
},
{
label: "Countryside",
icon: TbMountain,
description: "This property is in the countryside!",
},
{
label: "Pools",
icon: TbPool,
description: "This is property has a beautiful pool!",
},
{
label: "Islands",
icon: GiIsland,
description: "This property is on an island!",
},
{
label: "Lake",
icon: GiBoatFishing,
description: "This property is near a lake!",
},
{
label: "Skiing",
icon: FaSkiing,
description: "This property has skiing activies!",
},
{
label: "Castles",
icon: GiCastle,
description: "This property is an ancient castle!",
},
{
label: "Caves",
icon: GiCaveEntrance,
description: "This property is in a spooky cave!",
},
{
label: "Camping",
icon: GiForestCamp,
description: "This property offers camping activities!",
},
{
label: "Arctic",
icon: BsSnow,
description: "This property is in arctic environment!",
},
{
label: "Desert",
icon: GiCactus,
description: "This property is in the desert!",
},
{
label: "Barns",
icon: GiBarn,
description: "This property is in a barn!",
},
{
label: "Lux",
icon: IoDiamond,
description: "This property is brand new and luxurious!",
},
];
type Props = {};
function Categories({}: Props) {
const params = useSearchParams();
const category = params?.get("category");
const pathname = usePathname();
const isMainPage = pathname === "/";
if (!isMainPage) {
return null;
}
return (
<Container>
<div className="pt-4 flex flex-row items-center justify-between overflow-x-auto">
{categories.map((items, index) => (
<CategoryBox
key={index}
icon={items.icon}
label={items.label}
selected={category === items.label}
/>
))}
</div>
</Container>
);
}
export default Categories;
================================================
FILE: components/navbar/Logo.tsx
================================================
"use client";
import Image from "next/image";
import { useRouter } from "next/navigation";
import React from "react";
type Props = {};
function Logo({}: Props) {
const router = useRouter();
return (
<div onClick={() => router.push("/")}>
<Image
alt="logo"
className="hidden md:block cursor-pointer"
height="100"
width="100"
src="/assets/logo.png"
/>
</div>
);
}
export default Logo;
================================================
FILE: components/navbar/MenuItem.tsx
================================================
"use client";
import React from "react";
type Props = {
onClick: () => void;
label: string;
};
function MenuItem({ onClick, label }: Props) {
return (
<div
className=" px-4 py-3 hover:bg-neutral-100 transition font-semibold"
onClick={onClick}
>
{label}
</div>
);
}
export default MenuItem;
================================================
FILE: components/navbar/Navbar.tsx
================================================
"use client";
import { SafeUser } from "@/types";
import Container from "../Container";
import Logo from "./Logo";
import Search from "./Search";
import UserMenu from "./UserMenu";
import Categories from "./Categories";
type Props = {
currentUser?: SafeUser | null;
};
function Navbar({ currentUser }: Props) {
return (
<div className="fixed w-full bg-white z-10 shadow-sm">
<div className="py-4 border-b-[1px]">
<Container>
<div className="flex flex-row items-center justify-between gap-3 md:gap-0">
<Logo />
<Search />
<UserMenu currentUser={currentUser} />
</div>
</Container>
</div>
<Categories />
</div>
);
}
export default Navbar;
================================================
FILE: components/navbar/Search.tsx
================================================
"use client";
import useCountries from "@/hook/useCountries";
import useSearchModal from "@/hook/useSearchModal";
import { differenceInDays } from "date-fns";
import { useSearchParams } from "next/navigation";
import { useMemo } from "react";
import { BiSearch } from "react-icons/bi";
type Props = {};
function Search({}: Props) {
const searchModel = useSearchModal();
const params = useSearchParams();
const { getByValue } = useCountries();
const locationValue = params?.get("locationValue");
const startDate = params?.get("startDate");
const endDate = params?.get("endDate");
const guestCount = params?.get("guestCount");
const locationLabel = useMemo(() => {
if (locationValue) {
return getByValue(locationValue as string)?.label;
}
return "Anywhere";
}, [getByValue, locationValue]);
const durationLabel = useMemo(() => {
if (startDate && endDate) {
const start = new Date(startDate as string);
const end = new Date(endDate as string);
let diff = differenceInDays(end, start);
if (diff === 0) {
diff = 1;
}
return `${diff} Days`;
}
return "Any Week";
}, [startDate, endDate]);
const guessLabel = useMemo(() => {
if (guestCount) {
return `${guestCount} Guests`;
}
return "Add Guests";
}, []);
return (
<div
onClick={searchModel.onOpen}
className="border-[1px] w-full md:w-auto py-2 rounded-full shadow-sm hover:shadow-md transition cursor-pointer"
>
<div className="flex flex-row items-center justify-between">
<div className="text-sm font-semibold px-6">{locationLabel}</div>
<div className="hidden sm:block text-losm font-semibold px-6 border-x-[1px] flex-1 text-center">
{durationLabel}
</div>
<div className="text-sm pl-6 pr-2 text-gray-600 flex flex-row items-center gap-3">
<div className="hidden sm:block text-center">{guessLabel}</div>
<div className="p-2 bg-rose-500 rounded-full text-white">
<BiSearch size={18} />
</div>
</div>
</div>
</div>
);
}
export default Search;
================================================
FILE: components/navbar/UserMenu.tsx
================================================
"use client";
import useLoginModel from "@/hook/useLoginModal";
import useRegisterModal from "@/hook/useRegisterModal";
import useRentModal from "@/hook/useRentModal";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { SafeUser } from "@/types";
import { signOut } from "next-auth/react";
import { useCallback, useState } from "react";
import { AiOutlineMenu } from "react-icons/ai";
import Avatar from "../Avatar";
import MenuItem from "./MenuItem";
type Props = {
currentUser?: SafeUser | null;
};
function UserMenu({ currentUser }: Props) {
const router = useRouter();
const registerModel = useRegisterModal();
const loginModel = useLoginModel();
const rentModel = useRentModal();
const [isOpen, setIsOpen] = useState(false);
const toggleOpen = useCallback(() => {
setIsOpen((value) => !value);
}, []);
const onRent = useCallback(() => {
if (!currentUser) {
return loginModel.onOpen();
}
rentModel.onOpen();
}, [currentUser, loginModel, rentModel]);
return (
<div className="relative">
<div className="flex flex-row items-center gap-3">
<div
className="hidden md:block text-sm font-semibold py-3 px-4 rounded-full hover:bg-neutral-100 transition cursor-pointer"
onClick={onRent}
>
Airbnb your Home
</div>
<div
onClick={toggleOpen}
className="p-4 md:py-1 md:px-2 border-[1px] flex flex-row items-center gap-3 rounded-full cursor-pointer hover:shadow-md transition"
>
<AiOutlineMenu />
<div className="hidden md:block">
{currentUser ? (
<Avatar src={currentUser?.image!} userName={currentUser?.name} />
) : (
<Image
className="rounded-full"
height="30"
width="30"
alt="Avatar"
src="/assets/avatar.png"
/>
)}
</div>
</div>
</div>
{isOpen && (
<div className="absolute rounded-xl shadow-md w-[40vw] md:w-3/4 bg-white overflow-hidden right-0 top-12 text-sm">
<div className="flex flex-col cursor-pointer">
{currentUser ? (
<>
<MenuItem
onClick={() => router.push("/trips")}
label="My trips"
/>
<MenuItem
onClick={() => router.push("/favorites")}
label="My favorites"
/>
<MenuItem
onClick={() => router.push("/reservations")}
label="My reservations"
/>
<MenuItem
onClick={() => router.push("/properties")}
label="My properties"
/>
<MenuItem onClick={onRent} label="Airbnb your home" />
<hr />
<MenuItem onClick={() => signOut()} label="Logout" />
</>
) : (
<>
<MenuItem onClick={loginModel.onOpen} label="Login" />
<MenuItem onClick={registerModel.onOpen} label="Sign up" />
</>
)}
</div>
</div>
)}
</div>
);
}
export default UserMenu;
================================================
FILE: hook/useCountries.ts
================================================
import countries from "world-countries";
const formattedCountries = countries.map((country) => ({
value: country.cca2,
label: country.name.common,
flag: country.flag,
latlng: country.latlng,
region: country.region,
}));
const useCountries = () => {
const getAll = () => formattedCountries;
const getByValue = (value: string) => {
return formattedCountries.find((item) => item.value === value);
};
return {
getAll,
getByValue,
};
};
export default useCountries;
================================================
FILE: hook/useFavorite.ts
================================================
import { SafeUser } from "@/types";
import axios from "axios";
import { useRouter } from "next/navigation";
import { useCallback, useMemo } from "react";
import { toast } from "react-toastify";
import useLoginModel from "./useLoginModal";
type Props = {
listingId: string;
currentUser?: SafeUser | null;
};
function useFavorite({ listingId, currentUser }: Props) {
const router = useRouter();
const loginModel = useLoginModel();
const hasFavorite = useMemo(() => {
const list = currentUser?.favoriteIds || [];
return list.includes(listingId);
}, [currentUser, listingId]);
const toggleFavorite = useCallback(
async (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
if (!currentUser) {
return loginModel.onOpen();
}
try {
let request;
if (hasFavorite) {
request = () => axios.delete(`/api/favorites/${listingId}`);
} else {
request = () => axios.post(`/api/favorites/${listingId}`);
}
await request();
router.refresh();
toast.success("Success");
} catch (error: any) {
toast.error("Something Went Wrong");
}
},
[currentUser, hasFavorite, listingId, loginModel]
);
return {
hasFavorite,
toggleFavorite,
};
}
export default useFavorite;
================================================
FILE: hook/useLoginModal.ts
================================================
import { create } from "zustand";
interface LoginModelState {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
}
const useLoginModel = create<LoginModelState>((set) => ({
isOpen: false,
onOpen: () => set({ isOpen: true }),
onClose: () => set({ isOpen: false }),
}));
export default useLoginModel;
================================================
FILE: hook/useRegisterModal.ts
================================================
import { create } from "zustand";
interface RegisterModelStore {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
}
const useRegisterModal = create<RegisterModelStore>((set) => ({
isOpen: false,
onOpen: () => set({ isOpen: true }),
onClose: () => set({ isOpen: false }),
}));
export default useRegisterModal;
================================================
FILE: hook/useRentModal.ts
================================================
import { create } from "zustand";
interface RentModelStore {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
}
const useRentModal = create<RentModelStore>((set) => ({
isOpen: false,
onOpen: () => set({ isOpen: true }),
onClose: () => set({ isOpen: false }),
}));
export default useRentModal;
================================================
FILE: hook/useSearchModal.ts
================================================
import { create } from "zustand";
interface SearchModalStore {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
}
const useSearchModal = create<SearchModalStore>((set) => ({
isOpen: false,
onOpen: () => set({ isOpen: true }),
onClose: () => set({ isOpen: false }),
}));
export default useSearchModal;
================================================
FILE: lib/prismadb.ts
================================================
import { PrismaClient } from "@prisma/client";
declare global {
var prisma: PrismaClient | undefined;
}
const client = globalThis.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") globalThis.prisma = client;
export default client;
================================================
FILE: middleware.ts
================================================
export { default } from "next-auth/middleware";
export const config = {
matcher: ["/trips", "/reservations", "/properties", "/favorites"],
};
================================================
FILE: next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
},
images: {
domains: ["lh3.googleusercontent.com", "res.cloudinary.com"],
},
};
module.exports = nextConfig;
================================================
FILE: package.json
================================================
{
"name": "airbnb-clone",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@next-auth/prisma-adapter": "^1.0.5",
"@prisma/client": "^4.12.0",
"@types/date-fns": "^2.6.0",
"@types/node": "18.15.11",
"@types/react": "18.0.31",
"@types/react-dom": "18.0.11",
"axios": "^1.3.4",
"bcrypt": "^5.1.0",
"date-fns": "^2.29.3",
"eslint": "8.37.0",
"eslint-config-next": "13.2.4",
"framer-motion": "^10.10.0",
"leaflet": "^1.9.4",
"next": "13.2.4",
"next-auth": "^4.20.1",
"next-cloudinary": "^4.1.1",
"query-string": "^8.1.0",
"react": "18.2.0",
"react-date-range": "^1.4.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.9",
"react-icons": "^4.8.0",
"react-leaflet": "^4.2.1",
"react-select": "^5.7.2",
"react-toastify": "^9.1.2",
"react-world-flags": "^1.5.1",
"tailwind-scrollbar": "^3.0.0",
"typescript": "5.0.3",
"world-countries": "^4.0.0",
"zustand": "^4.3.7"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/leaflet": "^1.9.3",
"@types/react-date-range": "^1.4.4",
"@types/react-world-flags": "^1.4.2",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.21",
"prisma": "^4.12.0",
"tailwindcss": "^3.3.1"
}
}
================================================
FILE: pages/api/auth/[...nextauth].ts
================================================
import prisma from "@/lib/prismadb";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import bcrypt from "bcrypt";
import NextAuth, { AuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import FacebookProvider from "next-auth/providers/facebook";
import GoogleProvider from "next-auth/providers/google";
export const authOptions: AuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
}),
FacebookProvider({
clientId: process.env.FACEBOOK_ID as string,
clientSecret: process.env.FACEBOOK_SECRET as string,
}),
CredentialsProvider({
name: "credentials",
credentials: {
email: { label: "email", type: "text" },
password: { label: "password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
throw new Error("Invalid credentials");
}
const user = await prisma.user.findUnique({
where: {
email: credentials.email,
},
});
if (!user || !user?.hashedPassword) {
throw new Error("Invalid credentials");
}
const isCorrectPassword = await bcrypt.compare(
credentials.password,
user.hashedPassword
);
if (!isCorrectPassword) {
throw new Error("Invalid credentials");
}
return user;
},
}),
],
pages: {
signIn: "/",
},
debug: process.env.NODE_ENV === "development",
session: {
strategy: "jwt",
},
secret: process.env.NEXTAUTH_SECRET,
};
export default NextAuth(authOptions);
================================================
FILE: postcss.config.js
================================================
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
================================================
FILE: prisma/schema.prisma
================================================
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String?
email String? @unique
emailVerified DateTime?
image String?
hashedPassword String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
favoriteIds String[] @db.ObjectId
accounts Account[]
listings Listing[]
reservations Reservation[]
}
model Account {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String @db.ObjectId
type String
provider String
providerAccountId String
refresh_token String? @db.String
access_token String? @db.String
expires_at Int?
token_type String?
scope String?
id_token String? @db.String
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Listing {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
description String
imageSrc String
createdAt DateTime @default(now())
category String
roomCount Int
bathroomCount Int
guestCount Int
locationValue String
userId String @db.ObjectId
price Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
reservations Reservation[]
}
model Reservation {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String @db.ObjectId
listingId String @db.ObjectId
startDate DateTime
endDate DateTime
totalPrice Int
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade)
}
================================================
FILE: styles/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
@apply !scrollbar-thin !scrollbar-track-transparent !scrollbar-thumb-[#FF5A5F];
}
}
html,
body,
:root{
height: 100%;
}
.leaflet-bottom,
.leaflet-control,
.leaflet-pane,
.leaflet-top {
z-index: 0 !important;
}
.rdrMonth {
width: 100% !important;
}
.rdrCalendarWrapper {
font-size: 16px !important;
width: 100% !important;
}
================================================
FILE: tailwind.config.js
================================================
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [require("tailwind-scrollbar")],
};
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
================================================
FILE: types.ts
================================================
import { Listing, Reservation, User } from "@prisma/client";
export type safeListing = Omit<Listing, "createdAt"> & {
createdAt: string;
};
export type SafeReservation = Omit<
Reservation,
"createdAt" | "startDate" | "endDate" | "listing"
> & {
createdAt: string;
startDate: string;
endDate: string;
listing: safeListing;
};
export type SafeUser = Omit<
User,
"createdAt" | "updatedAt" | "emailVerified"
> & {
createdAt: string;
updatedAt: string;
emailVerified: string | null;
};
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
SYMBOL INDEX (128 symbols across 69 files)
FILE: app/actions/getCurrentUser.ts
function getSession (line 5) | async function getSession() {
function getCurrentUser (line 9) | async function getCurrentUser() {
FILE: app/actions/getFavoriteListings.ts
function getFavoriteListings (line 4) | async function getFavoriteListings() {
FILE: app/actions/getListingById.ts
type IParams (line 3) | interface IParams {
function getListingById (line 7) | async function getListingById(params: IParams) {
FILE: app/actions/getListings.ts
type IListingsParams (line 3) | interface IListingsParams {
function getListings (line 14) | async function getListings(params: IListingsParams) {
FILE: app/actions/getReservations.ts
type IParams (line 3) | interface IParams {
function getReservation (line 9) | async function getReservation(params: IParams) {
FILE: app/api/favorites/[listingId]/route.ts
type IPrisma (line 5) | interface IPrisma {
function POST (line 9) | async function POST(request: Request, { params }: { params: IPrisma }) {
function DELETE (line 38) | async function DELETE(
FILE: app/api/listings/[listingId]/route.ts
type IParams (line 5) | interface IParams {
function DELETE (line 9) | async function DELETE(
FILE: app/api/listings/route.ts
function POST (line 5) | async function POST(request: Request) {
FILE: app/api/register/route.ts
function POST (line 5) | async function POST(request: Request) {
FILE: app/api/reservations/[reservationId]/route.ts
type IParams (line 5) | interface IParams {
function DELETE (line 9) | async function DELETE(
FILE: app/api/reservations/route.ts
function POST (line 5) | async function POST(request: Request) {
FILE: app/error.tsx
type Props (line 6) | type Props = {
function ErrorState (line 10) | function ErrorState({ error }: Props) {
FILE: app/favorites/FavoritesClient.tsx
type Props (line 6) | type Props = {
function FavoritesClient (line 11) | function FavoritesClient({ listings, currentUser }: Props) {
FILE: app/favorites/page.tsx
type Props (line 8) | type Props = {};
FILE: app/layout.tsx
function RootLayout (line 23) | async function RootLayout({
FILE: app/listings/[listingId]/page.tsx
type IParams (line 8) | interface IParams {
FILE: app/loading.tsx
type Props (line 3) | type Props = {};
function Loading (line 5) | function Loading({}: Props) {
FILE: app/page.tsx
type HomeProps (line 8) | interface HomeProps {
function Home (line 12) | async function Home({ searchParams }: HomeProps) {
FILE: app/properties/PropertiesClient.tsx
type Props (line 12) | type Props = {
function PropertiesClient (line 17) | function PropertiesClient({ listings, currentUser }: Props) {
FILE: app/properties/page.tsx
type Props (line 7) | type Props = {};
FILE: app/reservations/ReservationsClient.tsx
type Props (line 13) | type Props = {
function ReservationsClient (line 18) | function ReservationsClient({ reservations, currentUser }: Props) {
FILE: app/reservations/page.tsx
type Props (line 8) | type Props = {};
FILE: app/trips/TripsClient.tsx
type Props (line 12) | type Props = {
function TripsClient (line 17) | function TripsClient({ reservations, currentUser }: Props) {
FILE: app/trips/page.tsx
type Props (line 8) | type Props = {};
FILE: components/Avatar.tsx
type Props (line 6) | type Props = {
function Avatar (line 11) | function Avatar({ src, userName }: Props) {
FILE: components/Button.tsx
type Props (line 6) | type Props = {
function Button (line 16) | function Button({
FILE: components/CategoryBox.tsx
type Props (line 8) | type Props = {
function CategoryBox (line 14) | function CategoryBox({ icon: Icon, label, selected }: Props) {
FILE: components/ClientOnly.tsx
type Props (line 6) | type Props = {
function ClientOnly (line 10) | function ClientOnly({ children }: Props) {
FILE: components/Container.tsx
type Props (line 5) | type Props = {
function Container (line 9) | function Container({ children }: Props) {
FILE: components/EmptyState.tsx
type Props (line 9) | type Props = {
function EmptyState (line 15) | function EmptyState({
FILE: components/Footer.tsx
type Props (line 8) | type Props = {};
function Footer (line 10) | function Footer({}: Props) {
FILE: components/FooterColumn.tsx
type Props (line 5) | type Props = {
function FooterColumn (line 10) | function FooterColumn({ index, data }: Props) {
FILE: components/Heading.tsx
type Props (line 5) | type Props = {
function Heading (line 11) | function Heading({ title, subtitle, center }: Props) {
FILE: components/HeartButton.tsx
type Props (line 8) | type Props = {
function HeartButton (line 13) | function HeartButton({ listingId, currentUser }: Props) {
FILE: components/ListingClient.tsx
type Props (line 24) | type Props = {
function ListingClient (line 32) | function ListingClient({ reservations = [], listing, currentUser }: Prop...
FILE: components/Loader.tsx
type Props (line 6) | type Props = {};
function Loader (line 8) | function Loader({}: Props) {
FILE: components/Map.tsx
type Props (line 21) | type Props = {
function Map (line 26) | function Map({ center, locationValue }: Props) {
FILE: components/Offers.tsx
type Props (line 60) | type Props = {};
function Offers (line 62) | function Offers({}: Props) {
FILE: components/Sleep.tsx
type Props (line 7) | type Props = {};
function Sleep (line 9) | function Sleep({}: Props) {
FILE: components/ToastContainerBar.tsx
type Props (line 8) | type Props = {};
function ToastContainerBar (line 10) | function ToastContainerBar({}: Props) {
FILE: components/inputs/Calendar.tsx
type Props (line 9) | type Props = {
function Calendar (line 15) | function Calendar({ value, onChange, disabledDates }: Props) {
FILE: components/inputs/CategoryInput.tsx
type Props (line 6) | type Props = {
function CategoryInput (line 13) | function CategoryInput({ icon: Icon, label, selected, onClick }: Props) {
FILE: components/inputs/Counter.tsx
type Props (line 6) | type Props = {
function Counter (line 13) | function Counter({ title, subtitle, value, onChange }: Props) {
FILE: components/inputs/CountrySelect.tsx
type CountrySelectValue (line 7) | type CountrySelectValue = {
type Props (line 15) | type Props = {
function CountrySelect (line 20) | function CountrySelect({ value, onChange }: Props) {
FILE: components/inputs/ImageUpload.tsx
type Props (line 12) | type Props = {
function ImageUpload (line 17) | function ImageUpload({ onChange, value }: Props) {
FILE: components/inputs/Input.tsx
type Props (line 5) | type Props = {
function Input (line 16) | function Input({
FILE: components/listing/ListingCard.tsx
type Props (line 13) | type Props = {
function ListingCard (line 23) | function ListingCard({
FILE: components/listing/ListingCategory.tsx
type Props (line 6) | type Props = {
function ListingCategory (line 12) | function ListingCategory({ icon: Icon, label, description }: Props) {
FILE: components/listing/ListingHead.tsx
type Props (line 10) | type Props = {
function ListingHead (line 18) | function ListingHead({
FILE: components/listing/ListingInfo.tsx
type Props (line 17) | type Props = {
function ListingInfo (line 33) | function ListingInfo({
FILE: components/listing/ListingReservation.tsx
type Props (line 8) | type Props = {
function ListingReservation (line 18) | function ListingReservation({
FILE: components/models/LoginModal.tsx
type Props (line 18) | type Props = {};
function LoginModal (line 20) | function LoginModal({}: Props) {
FILE: components/models/Modal.tsx
type Props (line 7) | type Props = {
function Modal (line 20) | function Modal({
FILE: components/models/RegisterModal.tsx
type Props (line 18) | type Props = {};
function RegisterModal (line 20) | function RegisterModal({}: Props) {
FILE: components/models/RentModal.tsx
type Props (line 20) | type Props = {};
type STEPS (line 22) | enum STEPS {
function RentModal (line 31) | function RentModal({}: Props) {
FILE: components/models/SearchModal.tsx
type STEPS (line 17) | enum STEPS {
type Props (line 23) | type Props = {};
function SearchModal (line 25) | function SearchModal({}: Props) {
FILE: components/navbar/Categories.tsx
type Props (line 100) | type Props = {};
function Categories (line 102) | function Categories({}: Props) {
FILE: components/navbar/Logo.tsx
type Props (line 7) | type Props = {};
function Logo (line 9) | function Logo({}: Props) {
FILE: components/navbar/MenuItem.tsx
type Props (line 5) | type Props = {
function MenuItem (line 10) | function MenuItem({ onClick, label }: Props) {
FILE: components/navbar/Navbar.tsx
type Props (line 10) | type Props = {
function Navbar (line 14) | function Navbar({ currentUser }: Props) {
FILE: components/navbar/Search.tsx
type Props (line 10) | type Props = {};
function Search (line 12) | function Search({}: Props) {
FILE: components/navbar/UserMenu.tsx
type Props (line 16) | type Props = {
function UserMenu (line 20) | function UserMenu({ currentUser }: Props) {
FILE: hook/useFavorite.ts
type Props (line 8) | type Props = {
function useFavorite (line 13) | function useFavorite({ listingId, currentUser }: Props) {
FILE: hook/useLoginModal.ts
type LoginModelState (line 3) | interface LoginModelState {
FILE: hook/useRegisterModal.ts
type RegisterModelStore (line 3) | interface RegisterModelStore {
FILE: hook/useRentModal.ts
type RentModelStore (line 3) | interface RentModelStore {
FILE: hook/useSearchModal.ts
type SearchModalStore (line 3) | interface SearchModalStore {
FILE: pages/api/auth/[...nextauth].ts
method authorize (line 26) | async authorize(credentials) {
FILE: types.ts
type safeListing (line 3) | type safeListing = Omit<Listing, "createdAt"> & {
type SafeReservation (line 7) | type SafeReservation = Omit<
type SafeUser (line 17) | type SafeUser = Omit<
Condensed preview — 84 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (126K chars).
[
{
"path": ".eslintrc.json",
"chars": 40,
"preview": "{\n \"extends\": \"next/core-web-vitals\"\n}\n"
},
{
"path": ".gitignore",
"chars": 390,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": ".vscode/settings.json",
"chars": 107,
"preview": "{\n \"typescript.tsdk\": \"node_modules\\\\typescript\\\\lib\",\n \"typescript.enablePromptUseWorkspaceTsdk\": true\n}"
},
{
"path": "README.md",
"chars": 10344,
"preview": "<div align=\"center\">\n\n <img src=\"https://user-images.githubusercontent.com/99184393/185779974-a31a9f47-f8d3-42ea-b7f8-4"
},
{
"path": "app/actions/getCurrentUser.ts",
"chars": 931,
"preview": "import prisma from \"@/lib/prismadb\";\nimport { authOptions } from \"@/pages/api/auth/[...nextauth]\";\nimport { getServerSes"
},
{
"path": "app/actions/getFavoriteListings.ts",
"chars": 633,
"preview": "import prisma from \"@/lib/prismadb\";\nimport getCurrentUser from \"./getCurrentUser\";\n\nexport default async function getFa"
},
{
"path": "app/actions/getListingById.ts",
"chars": 771,
"preview": "import prisma from \"@/lib/prismadb\";\n\ninterface IParams {\n listingId?: string;\n}\n\nexport default async function getList"
},
{
"path": "app/actions/getListings.ts",
"chars": 1755,
"preview": "import prisma from \"@/lib/prismadb\";\n\nexport interface IListingsParams {\n userId?: string;\n guestCount?: number;\n roo"
},
{
"path": "app/actions/getReservations.ts",
"chars": 1128,
"preview": "import prisma from \"@/lib/prismadb\";\n\ninterface IParams {\n listingId?: string;\n userId?: string;\n authorId?: string;\n"
},
{
"path": "app/api/favorites/[listingId]/route.ts",
"chars": 1372,
"preview": "import getCurrentUser from \"@/app/actions/getCurrentUser\";\nimport prisma from \"@/lib/prismadb\";\nimport { NextResponse } "
},
{
"path": "app/api/listings/[listingId]/route.ts",
"chars": 676,
"preview": "import { NextResponse } from \"next/server\";\nimport getCurrentUser from \"@/app/actions/getCurrentUser\";\nimport prisma fro"
},
{
"path": "app/api/listings/route.ts",
"chars": 929,
"preview": "import getCurrentUser from \"@/app/actions/getCurrentUser\";\nimport prisma from \"@/lib/prismadb\";\nimport { NextResponse } "
},
{
"path": "app/api/register/route.ts",
"chars": 448,
"preview": "import prisma from \"@/lib/prismadb\";\nimport bcrypt from \"bcrypt\";\nimport { NextResponse } from \"next/server\";\n\nexport as"
},
{
"path": "app/api/reservations/[reservationId]/route.ts",
"chars": 759,
"preview": "import getCurrentUser from \"@/app/actions/getCurrentUser\";\nimport prisma from \"@/lib/prismadb\";\nimport { NextResponse } "
},
{
"path": "app/api/reservations/route.ts",
"chars": 831,
"preview": "import getCurrentUser from \"@/app/actions/getCurrentUser\";\nimport prisma from \"@/lib/prismadb\";\nimport { NextResponse } "
},
{
"path": "app/error.tsx",
"chars": 386,
"preview": "\"use client\";\n\nimport EmptyState from \"@/components/EmptyState\";\nimport { useEffect } from \"react\";\n\ntype Props = {\n er"
},
{
"path": "app/favorites/FavoritesClient.tsx",
"chars": 824,
"preview": "import Container from \"@/components/Container\";\nimport Heading from \"@/components/Heading\";\nimport ListingCard from \"@/c"
},
{
"path": "app/favorites/page.tsx",
"chars": 995,
"preview": "import ClientOnly from \"@/components/ClientOnly\";\nimport EmptyState from \"@/components/EmptyState\";\nimport React from \"r"
},
{
"path": "app/layout.tsx",
"chars": 1321,
"preview": "import ClientOnly from \"@/components/ClientOnly\";\nimport Footer from \"@/components/Footer\";\nimport ToastContainerBar fro"
},
{
"path": "app/listings/[listingId]/page.tsx",
"chars": 907,
"preview": "import getCurrentUser from \"@/app/actions/getCurrentUser\";\nimport getListingById from \"@/app/actions/getListingById\";\nim"
},
{
"path": "app/loading.tsx",
"chars": 139,
"preview": "import Loader from \"@/components/Loader\";\n\ntype Props = {};\n\nfunction Loading({}: Props) {\n return <Loader />;\n}\n\nexpor"
},
{
"path": "app/page.tsx",
"chars": 1166,
"preview": "import ClientOnly from \"@/components/ClientOnly\";\nimport Container from \"@/components/Container\";\nimport EmptyState from"
},
{
"path": "app/properties/PropertiesClient.tsx",
"chars": 1657,
"preview": "\"use client\";\n\nimport Container from \"@/components/Container\";\nimport Heading from \"@/components/Heading\";\nimport Listin"
},
{
"path": "app/properties/page.tsx",
"chars": 975,
"preview": "import ClientOnly from \"@/components/ClientOnly\";\nimport EmptyState from \"@/components/EmptyState\";\nimport getCurrentUse"
},
{
"path": "app/reservations/ReservationsClient.tsx",
"chars": 1769,
"preview": "\"use client\";\n\nimport { SafeReservation, SafeUser } from \"@/types\";\nimport axios from \"axios\";\nimport { useRouter } from"
},
{
"path": "app/reservations/page.tsx",
"chars": 1088,
"preview": "import ClientOnly from \"@/components/ClientOnly\";\nimport EmptyState from \"@/components/EmptyState\";\nimport React from \"r"
},
{
"path": "app/trips/TripsClient.tsx",
"chars": 1782,
"preview": "\"use client\";\n\nimport Container from \"@/components/Container\";\nimport Heading from \"@/components/Heading\";\nimport Listin"
},
{
"path": "app/trips/page.tsx",
"chars": 1009,
"preview": "import ClientOnly from \"@/components/ClientOnly\";\nimport EmptyState from \"@/components/EmptyState\";\nimport React from \"r"
},
{
"path": "components/Avatar.tsx",
"chars": 810,
"preview": "\"use client\";\n\nimport Image from \"next/image\";\nimport React from \"react\";\n\ntype Props = {\n src: string | null | undefin"
},
{
"path": "components/Button.tsx",
"chars": 1115,
"preview": "\"use client\";\n\nimport React from \"react\";\nimport { IconType } from \"react-icons\";\n\ntype Props = {\n label: string;\n onC"
},
{
"path": "components/CategoryBox.tsx",
"chars": 1347,
"preview": "\"use client\";\n\nimport { useRouter, useSearchParams } from \"next/navigation\";\nimport qs from \"query-string\";\nimport React"
},
{
"path": "components/ClientOnly.tsx",
"chars": 555,
"preview": "\"use client\";\n\nimport { motion } from \"framer-motion\";\nimport React, { useEffect, useState } from \"react\";\n\ntype Props ="
},
{
"path": "components/Container.tsx",
"chars": 282,
"preview": "\"use client\";\n\nimport React from \"react\";\n\ntype Props = {\n children: React.ReactNode;\n};\n\nfunction Container({ children"
},
{
"path": "components/EmptyState.tsx",
"chars": 979,
"preview": "\"use client\";\n\nimport { motion } from \"framer-motion\";\nimport { useRouter } from \"next/navigation\";\nimport React from \"r"
},
{
"path": "components/Footer.tsx",
"chars": 1390,
"preview": "\"use client\";\n\nimport { motion } from \"framer-motion\";\nimport React, { useEffect, useState } from \"react\";\nimport Client"
},
{
"path": "components/FooterColumn.tsx",
"chars": 655,
"preview": "\"user client\"\n\nimport { motion } from \"framer-motion\";\n\ntype Props = {\n index: number;\n data: Array<string>;\n };\n"
},
{
"path": "components/Heading.tsx",
"chars": 418,
"preview": "\"use client\";\n\nimport React from \"react\";\n\ntype Props = {\n title: string;\n subtitle?: string;\n center?: boolean;\n};\n\n"
},
{
"path": "components/HeartButton.tsx",
"chars": 821,
"preview": "\"use client\";\n\nimport useFavorite from \"@/hook/useFavorite\";\nimport { SafeUser } from \"@/types\";\nimport React from \"reac"
},
{
"path": "components/ListingClient.tsx",
"chars": 4094,
"preview": "\"use client\";\n\nimport useLoginModel from \"@/hook/useLoginModal\";\nimport { SafeReservation, SafeUser, safeListing } from "
},
{
"path": "components/Loader.tsx",
"chars": 2195,
"preview": "\"use client\";\n\nimport { motion } from \"framer-motion\";\nimport React from \"react\";\n\ntype Props = {};\n\nfunction Loader({}:"
},
{
"path": "components/Map.tsx",
"chars": 1631,
"preview": "\"use client\";\n\nimport L from \"leaflet\";\nimport React from \"react\";\nimport { MapContainer, Marker, Popup, TileLayer } fro"
},
{
"path": "components/Offers.tsx",
"chars": 2663,
"preview": "\"use client\";\n\nimport { motion } from \"framer-motion\";\nimport { AiOutlineCar, AiOutlineWifi } from \"react-icons/ai\";\nimp"
},
{
"path": "components/Sleep.tsx",
"chars": 2261,
"preview": "\"use client\";\n\nimport { motion } from \"framer-motion\";\nimport { BiBed } from \"react-icons/bi\";\nimport { IoBedOutline } f"
},
{
"path": "components/ToastContainerBar.tsx",
"chars": 505,
"preview": "\"use client\";\n\nimport React from \"react\";\nimport { ToastContainer } from \"react-toastify\";\n\nimport \"react-toastify/dist/"
},
{
"path": "components/inputs/Calendar.tsx",
"chars": 662,
"preview": "\"use client\";\n\nimport React from \"react\";\nimport { DateRange, Range, RangeKeyDict } from \"react-date-range\";\n\nimport \"re"
},
{
"path": "components/inputs/CategoryInput.tsx",
"chars": 635,
"preview": "\"use client\";\n\nimport React from \"react\";\nimport { IconType } from \"react-icons\";\n\ntype Props = {\n icon: IconType;\n la"
},
{
"path": "components/inputs/Counter.tsx",
"chars": 1481,
"preview": "\"use client\";\n\nimport React, { useCallback } from \"react\";\nimport { AiOutlineMinus, AiOutlinePlus } from \"react-icons/ai"
},
{
"path": "components/inputs/CountrySelect.tsx",
"chars": 1407,
"preview": "\"use client\";\n\nimport useCountries from \"@/hook/useCountries\";\nimport Select from \"react-select\";\nimport Flag from \"reac"
},
{
"path": "components/inputs/ImageUpload.tsx",
"chars": 1432,
"preview": "\"use client\";\n\nimport { CldUploadWidget } from \"next-cloudinary\";\nimport Image from \"next/image\";\nimport React, { useCal"
},
{
"path": "components/inputs/Input.tsx",
"chars": 1656,
"preview": "import React from \"react\";\nimport { FieldErrors, FieldValues, UseFormRegister } from \"react-hook-form\";\nimport { BiDolla"
},
{
"path": "components/listing/ListingCard.tsx",
"chars": 3043,
"preview": "\"use client\";\n\nimport useCountries from \"@/hook/useCountries\";\nimport { SafeReservation, SafeUser, safeListing } from \"@"
},
{
"path": "components/listing/ListingCategory.tsx",
"chars": 645,
"preview": "\"use client\";\n\nimport React from \"react\";\nimport { IconType } from \"react-icons\";\n\ntype Props = {\n icon: IconType;\n la"
},
{
"path": "components/listing/ListingHead.tsx",
"chars": 1312,
"preview": "\"use client\";\n\nimport useCountries from \"@/hook/useCountries\";\nimport { SafeUser } from \"@/types\";\nimport { motion } fro"
},
{
"path": "components/listing/ListingInfo.tsx",
"chars": 2491,
"preview": "\"use client\";\n\nimport useCountries from \"@/hook/useCountries\";\nimport { SafeUser } from \"@/types\";\nimport dynamic from \""
},
{
"path": "components/listing/ListingReservation.tsx",
"chars": 1318,
"preview": "\"use client\";\n\nimport React from \"react\";\nimport { Range } from \"react-date-range\";\nimport Calendar from \"../inputs/Cale"
},
{
"path": "components/models/LoginModal.tsx",
"chars": 3084,
"preview": "\"use client\";\n\nimport useLoginModel from \"@/hook/useLoginModal\";\nimport useRegisterModal from \"@/hook/useRegisterModal\";"
},
{
"path": "components/models/Modal.tsx",
"chars": 3173,
"preview": "\"use client\";\n\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport { IoMdClose } from \"react-icons/i"
},
{
"path": "components/models/RegisterModal.tsx",
"chars": 3254,
"preview": "\"use client\";\n\nimport useLoginModel from \"@/hook/useLoginModal\";\nimport useRegisterModal from \"@/hook/useRegisterModal\";"
},
{
"path": "components/models/RentModal.tsx",
"chars": 6676,
"preview": "\"use client\";\n\nimport useRentModal from \"@/hook/useRentModal\";\nimport axios from \"axios\";\nimport dynamic from \"next/dyna"
},
{
"path": "components/models/SearchModal.tsx",
"chars": 4537,
"preview": "\"use client\";\n\nimport useSearchModal from \"@/hook/useSearchModal\";\nimport { formatISO } from \"date-fns\";\nimport dynamic "
},
{
"path": "components/navbar/Categories.tsx",
"chars": 2823,
"preview": "\"use client\";\n\nimport { usePathname, useSearchParams } from \"next/navigation\";\nimport { BsSnow } from \"react-icons/bs\";\n"
},
{
"path": "components/navbar/Logo.tsx",
"chars": 454,
"preview": "\"use client\";\n\nimport Image from \"next/image\";\nimport { useRouter } from \"next/navigation\";\nimport React from \"react\";\n\n"
},
{
"path": "components/navbar/MenuItem.tsx",
"chars": 332,
"preview": "\"use client\";\n\nimport React from \"react\";\n\ntype Props = {\n onClick: () => void;\n label: string;\n};\n\nfunction MenuItem("
},
{
"path": "components/navbar/Navbar.tsx",
"chars": 745,
"preview": "\"use client\";\n\nimport { SafeUser } from \"@/types\";\nimport Container from \"../Container\";\nimport Logo from \"./Logo\";\nimpo"
},
{
"path": "components/navbar/Search.tsx",
"chars": 2149,
"preview": "\"use client\";\n\nimport useCountries from \"@/hook/useCountries\";\nimport useSearchModal from \"@/hook/useSearchModal\";\nimpor"
},
{
"path": "components/navbar/UserMenu.tsx",
"chars": 3321,
"preview": "\"use client\";\n\nimport useLoginModel from \"@/hook/useLoginModal\";\nimport useRegisterModal from \"@/hook/useRegisterModal\";"
},
{
"path": "hook/useCountries.ts",
"chars": 498,
"preview": "import countries from \"world-countries\";\n\nconst formattedCountries = countries.map((country) => ({\n value: country.cca2"
},
{
"path": "hook/useFavorite.ts",
"chars": 1335,
"preview": "import { SafeUser } from \"@/types\";\nimport axios from \"axios\";\nimport { useRouter } from \"next/navigation\";\nimport { use"
},
{
"path": "hook/useLoginModal.ts",
"chars": 321,
"preview": "import { create } from \"zustand\";\n\ninterface LoginModelState {\n isOpen: boolean;\n onOpen: () => void;\n onClose: () =>"
},
{
"path": "hook/useRegisterModal.ts",
"chars": 333,
"preview": "import { create } from \"zustand\";\n\ninterface RegisterModelStore {\n isOpen: boolean;\n onOpen: () => void;\n onClose: ()"
},
{
"path": "hook/useRentModal.ts",
"chars": 317,
"preview": "import { create } from \"zustand\";\n\ninterface RentModelStore {\n isOpen: boolean;\n onOpen: () => void;\n onClose: () => "
},
{
"path": "hook/useSearchModal.ts",
"chars": 325,
"preview": "import { create } from \"zustand\";\n\ninterface SearchModalStore {\n isOpen: boolean;\n onOpen: () => void;\n onClose: () ="
},
{
"path": "lib/prismadb.ts",
"chars": 260,
"preview": "import { PrismaClient } from \"@prisma/client\";\n\ndeclare global {\n var prisma: PrismaClient | undefined;\n}\n\nconst client"
},
{
"path": "middleware.ts",
"chars": 145,
"preview": "export { default } from \"next-auth/middleware\";\n\nexport const config = {\n matcher: [\"/trips\", \"/reservations\", \"/proper"
},
{
"path": "next.config.js",
"chars": 219,
"preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n experimental: {\n appDir: true,\n },\n images: {\n d"
},
{
"path": "package.json",
"chars": 1407,
"preview": "{\n \"name\": \"airbnb-clone\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev\",\n \"build\": "
},
{
"path": "pages/api/auth/[...nextauth].ts",
"chars": 1803,
"preview": "import prisma from \"@/lib/prismadb\";\nimport { PrismaAdapter } from \"@next-auth/prisma-adapter\";\nimport bcrypt from \"bcry"
},
{
"path": "postcss.config.js",
"chars": 82,
"preview": "module.exports = {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n}\n"
},
{
"path": "prisma/schema.prisma",
"chars": 2203,
"preview": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator clien"
},
{
"path": "styles/globals.css",
"chars": 435,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n body {\n @apply !scrollbar-thin !scrollbar"
},
{
"path": "tailwind.config.js",
"chars": 271,
"preview": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n content: [\n \"./app/**/*.{js,ts,jsx,tsx}\",\n \"./pag"
},
{
"path": "tsconfig.json",
"chars": 638,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"es5\",\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n \"sk"
},
{
"path": "types.ts",
"chars": 508,
"preview": "import { Listing, Reservation, User } from \"@prisma/client\";\n\nexport type safeListing = Omit<Listing, \"createdAt\"> & {\n "
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the SashenJayathilaka/Airbnb-Build GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 84 files (111.9 KB), approximately 30.9k tokens, and a symbol index with 128 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.