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
================================================
# Airbnb Clone with Next.js 13!
Full Stack Airbnb Clone with Next.js 13 Tailwind-css, Prisma, MongoDB, NextAuth, Framer-motionSocial, Login (Google and Facebook), Image upload, Cloudinary CDN, Location selection, Map component, Country autocomplete, Fetching listings with server components.






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



### :space_invader: Tech Stack
Client
Database
## :toolbox: Getting Started
### :bangbang: Prerequisites
- Install Node JS in your computer HERE
- Sign up for a Cloudinary account HERE
- Sign up for a Google Cloud Platform HERE
- Sign up for a Meta for Developers HERE
- Get Lookup APi Key HERE
### :key: Environment Variables
To run this project, you will need to add the following environment variables to your .env file
`DATABASE_URL`
`GOOGLE_CLIENT_ID`
`GOOGLE_CLIENT_SECRET`
`FACEBOOK_ID`
`FACEBOOK_SECRET`
`NEXTAUTH_SECRET`
`NEXTAUTH_URL`
`NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME`
`NEXT_PUBLIC_LOOKUP_KEY`
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
### :gear: Installation


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.
```js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
// Or if using `src` directory:
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
};
```
#### Add the Tailwind directives to your CSS
Add the `@tailwind` directives for each of Tailwind’s layers to your `./styles/globals.css` file.
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
Install dependencies
🔶 Dependency Info
### :running: Run Locally

Clone the project
```bash
git clone https://github.com/SashenJayathilaka/Airbnb-Build.git
```
change directory
```bash
cd Airbnb-Build
```
Install dependencies
```bash
npm install
```
Start the server
```bash
npm run dev
```
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
### Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
### :triangular_flag_on_post: Deployment
To deploy this project run
##### Deploy on Vercel

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)
Don't forget to leave a star ⭐️
================================================
FILE: app/actions/getCurrentUser.ts
================================================
import prisma from "@/lib/prismadb";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth/next";
export async function getSession() {
return await getServerSession(authOptions);
}
export default async function getCurrentUser() {
try {
const session = await getSession();
if (!session?.user?.email) {
return null;
}
const currentUser = await prisma.user.findUnique({
where: {
email: session.user.email as string,
},
});
if (!currentUser) {
return null;
}
return {
...currentUser,
createdAt: currentUser.createdAt.toISOString(),
updatedAt: currentUser.updatedAt.toISOString(),
emailVerified: currentUser.emailVerified?.toISOString() || null,
};
} catch (error: any) {
console.log(
"🚀 ~ file: getCurrentUser.ts:13 ~ getCurrentUser ~ error:",
error
);
}
}
================================================
FILE: app/actions/getFavoriteListings.ts
================================================
import prisma from "@/lib/prismadb";
import getCurrentUser from "./getCurrentUser";
export default async function getFavoriteListings() {
try {
const currentUser = await getCurrentUser();
if (!currentUser) {
return [];
}
const favorites = await prisma.listing.findMany({
where: {
id: {
in: [...(currentUser.favoriteIds || [])],
},
},
});
const safeFavorite = favorites.map((favorite) => ({
...favorite,
createdAt: favorite.createdAt.toString(),
}));
return safeFavorite;
} catch (error: any) {
throw new Error(error.message);
}
}
================================================
FILE: app/actions/getListingById.ts
================================================
import prisma from "@/lib/prismadb";
interface IParams {
listingId?: string;
}
export default async function getListingById(params: IParams) {
try {
const { listingId } = params;
const listing = await prisma.listing.findUnique({
where: {
id: listingId,
},
include: {
user: true,
},
});
if (!listing) {
return null;
}
return {
...listing,
createdAt: listing.createdAt.toString(),
user: {
...listing.user,
createdAt: listing.user.createdAt.toString(),
updatedAt: listing.user.updatedAt.toString(),
emailVerified: listing.user.emailVerified?.toString() || null,
},
};
} catch (error: any) {
throw new Error(error.message);
}
}
================================================
FILE: app/actions/getListings.ts
================================================
import prisma from "@/lib/prismadb";
export interface IListingsParams {
userId?: string;
guestCount?: number;
roomCount?: number;
bathroomCount?: number;
startDate?: string;
endDate?: string;
locationValue?: string;
category?: string;
}
export default async function getListings(params: IListingsParams) {
try {
const {
userId,
roomCount,
guestCount,
bathroomCount,
locationValue,
startDate,
endDate,
category,
} = params;
let query: any = {};
if (userId) {
query.userId = userId;
}
if (category) {
query.category = category;
}
if (roomCount) {
query.roomCount = {
gte: +roomCount,
};
}
if (guestCount) {
query.guestCount = {
gte: +guestCount,
};
}
if (bathroomCount) {
query.bathroomCount = {
gte: +bathroomCount,
};
}
if (locationValue) {
query.locationValue = locationValue;
}
if (startDate && endDate) {
query.NOT = {
reservations: {
some: {
OR: [
{
endDate: { gte: startDate },
startDate: { lte: startDate },
},
{
startDate: { lte: endDate },
endDate: { gte: endDate },
},
],
},
},
};
}
const listing = await prisma.listing.findMany({
where: query,
orderBy: {
createdAt: "desc",
},
});
const safeListings = listing.map((list) => ({
...list,
createdAt: list.createdAt.toISOString(),
}));
return safeListings;
} catch (error: any) {
throw new Error(error.message);
}
}
================================================
FILE: app/actions/getReservations.ts
================================================
import prisma from "@/lib/prismadb";
interface IParams {
listingId?: string;
userId?: string;
authorId?: string;
}
export default async function getReservation(params: IParams) {
try {
const { listingId, userId, authorId } = params;
const query: any = {};
if (listingId) {
query.listingId = listingId;
}
if (userId) {
query.userId = userId;
}
if (authorId) {
query.listing = { userId: authorId };
}
const reservation = await prisma.reservation.findMany({
where: query,
include: {
listing: true,
},
orderBy: {
createdAt: "desc",
},
});
const safeReservations = reservation.map((reservation) => ({
...reservation,
createdAt: reservation.createdAt.toISOString(),
startDate: reservation.startDate.toISOString(),
endDate: reservation.endDate.toISOString(),
listing: {
...reservation.listing,
createdAt: reservation.listing.createdAt.toISOString(),
},
}));
return safeReservations;
} catch (error: any) {
throw new Error(error.message);
}
}
================================================
FILE: app/api/favorites/[listingId]/route.ts
================================================
import getCurrentUser from "@/app/actions/getCurrentUser";
import prisma from "@/lib/prismadb";
import { NextResponse } from "next/server";
interface IPrisma {
listingId?: string;
}
export async function POST(request: Request, { params }: { params: IPrisma }) {
const currentUser = await getCurrentUser();
if (!currentUser) {
return NextResponse.error();
}
const { listingId } = params;
if (!listingId || typeof listingId !== "string") {
throw new Error("Invalid Id");
}
let favoriteIds = [...(currentUser.favoriteIds || [])];
favoriteIds.push(listingId);
const user = await prisma.user.update({
where: {
id: currentUser.id,
},
data: {
favoriteIds,
},
});
return NextResponse.json(user);
}
export async function DELETE(
request: Request,
{ params }: { params: IPrisma }
) {
const currentUser = await getCurrentUser();
if (!currentUser) {
return NextResponse.error();
}
const { listingId } = params;
if (!listingId || typeof listingId !== "string") {
throw new Error("Invalid Id");
}
let favoriteIds = [...(currentUser.favoriteIds || [])];
favoriteIds = favoriteIds.filter((id) => id !== listingId);
const user = await prisma.user.update({
where: {
id: currentUser.id,
},
data: {
favoriteIds,
},
});
return NextResponse.json(user);
}
================================================
FILE: app/api/listings/[listingId]/route.ts
================================================
import { NextResponse } from "next/server";
import getCurrentUser from "@/app/actions/getCurrentUser";
import prisma from "@/lib/prismadb";
interface IParams {
listingId?: string;
}
export async function DELETE(
request: Request,
{ params }: { params: IParams }
) {
const currentUser = await getCurrentUser();
if (!currentUser) {
return NextResponse.error();
}
const { listingId } = params;
if (!listingId || typeof listingId !== "string") {
throw new Error("Invalid Id");
}
const listing = await prisma.listing.deleteMany({
where: {
id: listingId,
userId: currentUser.id,
},
});
return NextResponse.json(listing);
}
================================================
FILE: app/api/listings/route.ts
================================================
import getCurrentUser from "@/app/actions/getCurrentUser";
import prisma from "@/lib/prismadb";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const currentUser = await getCurrentUser();
if (!currentUser) {
return NextResponse.error();
}
const body = await request.json();
const {
title,
description,
imageSrc,
category,
roomCount,
bathroomCount,
guestCount,
location,
price,
} = body;
Object.keys(body).forEach((value: any) => {
if (!body[value]) {
NextResponse.error();
}
});
const listen = await prisma.listing.create({
data: {
title,
description,
imageSrc,
category,
roomCount,
bathroomCount,
guestCount,
locationValue: location.value,
price: parseInt(price, 10),
userId: currentUser.id,
},
});
return NextResponse.json(listen);
}
================================================
FILE: app/api/register/route.ts
================================================
import prisma from "@/lib/prismadb";
import bcrypt from "bcrypt";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const body = await request.json();
const { email, name, password } = body;
const hashedPassword = await bcrypt.hash(password, 12);
const user = await prisma.user.create({
data: {
email,
name,
hashedPassword,
},
});
return NextResponse.json(user);
}
================================================
FILE: app/api/reservations/[reservationId]/route.ts
================================================
import getCurrentUser from "@/app/actions/getCurrentUser";
import prisma from "@/lib/prismadb";
import { NextResponse } from "next/server";
interface IParams {
reservationId?: string;
}
export async function DELETE(
request: Request,
{ params }: { params: IParams }
) {
const currentUser = await getCurrentUser();
if (!currentUser) {
return NextResponse.error();
}
const { reservationId } = params;
if (!reservationId || typeof reservationId !== "string") {
throw new Error("Invalid Id");
}
const reservation = await prisma.reservation.deleteMany({
where: {
id: reservationId,
OR: [{ userId: currentUser.id }, { listing: { userId: currentUser.id } }],
},
});
return NextResponse.json(reservation);
}
================================================
FILE: app/api/reservations/route.ts
================================================
import getCurrentUser from "@/app/actions/getCurrentUser";
import prisma from "@/lib/prismadb";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const currentUser = await getCurrentUser();
if (!currentUser) {
return NextResponse.error();
}
const body = await request.json();
const { listingId, startDate, endDate, totalPrice } = body;
if (!listingId || !startDate || !endDate || !totalPrice) {
return NextResponse.error();
}
const listenAndReservation = await prisma.listing.update({
where: {
id: listingId,
},
data: {
reservations: {
create: {
userId: currentUser.id,
startDate,
endDate,
totalPrice,
},
},
},
});
return NextResponse.json(listenAndReservation);
}
================================================
FILE: app/error.tsx
================================================
"use client";
import EmptyState from "@/components/EmptyState";
import { useEffect } from "react";
type Props = {
error: Error;
};
function ErrorState({ error }: Props) {
useEffect(() => {
console.log("🚀 ~ file: error.tsx:12 ~ ErrorState ~ error:", error);
}, [error]);
return ;
}
export default ErrorState;
================================================
FILE: app/favorites/FavoritesClient.tsx
================================================
import Container from "@/components/Container";
import Heading from "@/components/Heading";
import ListingCard from "@/components/listing/ListingCard";
import { SafeUser, safeListing } from "@/types";
type Props = {
listings: safeListing[];
currentUser?: SafeUser | null;
};
function FavoritesClient({ listings, currentUser }: Props) {
return (
{listings.map((listing) => (
))}
);
}
export default FavoritesClient;
================================================
FILE: app/favorites/page.tsx
================================================
import ClientOnly from "@/components/ClientOnly";
import EmptyState from "@/components/EmptyState";
import React from "react";
import getCurrentUser from "../actions/getCurrentUser";
import getFavoriteListings from "../actions/getFavoriteListings";
import FavoritesClient from "./FavoritesClient";
type Props = {};
const FavoritePage = async (props: Props) => {
const currentUser = await getCurrentUser();
const listings = await getFavoriteListings();
if (!currentUser) {
return (
);
}
if (listings.length === 0) {
return (
);
}
return (
);
};
export default FavoritePage;
================================================
FILE: app/layout.tsx
================================================
import ClientOnly from "@/components/ClientOnly";
import Footer from "@/components/Footer";
import ToastContainerBar from "@/components/ToastContainerBar";
import LoginModal from "@/components/models/LoginModal";
import RegisterModal from "@/components/models/RegisterModal";
import RentModal from "@/components/models/RentModal";
import SearchModal from "@/components/models/SearchModal";
import Navbar from "@/components/navbar/Navbar";
import { Nunito } from "next/font/google";
import "../styles/globals.css";
import getCurrentUser from "./actions/getCurrentUser";
export const metadata = {
title: "Airbnb Clone",
description: "Airbnb Clone",
icons: "https://www.seekpng.com/png/full/957-9571167_airbnb-png.png",
};
const font = Nunito({
subsets: ["latin"],
});
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const currentUser = await getCurrentUser();
return (
{children}
);
}
================================================
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 (
);
}
return (
);
};
export default ListingPage;
================================================
FILE: app/loading.tsx
================================================
import Loader from "@/components/Loader";
type Props = {};
function Loading({}: Props) {
return ;
}
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 (
);
}
return (
{listing.map((list) => {
return (
);
})}
);
}
================================================
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 (
{listings.map((listing: any) => (
))}
);
}
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 (
);
}
const listings = await getListings({ userId: currentUser.id });
if (listings.length === 0) {
return (
);
}
return (
);
};
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 (
{reservations.map((reservation) => (
))}
);
}
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 (
);
}
const reservations = await getReservation({
authorId: currentUser.id,
});
if (reservations.length === 0) {
return (
);
}
return (
);
};
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 (
{reservations.map((reservation) => (
))}
);
}
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 (
);
}
const reservations = await getReservation({
userId: currentUser.id,
});
if (reservations.length === 0) {
return (
);
}
return (
);
};
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 (
{src ? (
) : userName ? (
) : (
)}
);
}
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) => void;
disabled?: boolean;
outline?: boolean;
small?: boolean;
icon?: IconType;
isColor?: boolean;
};
function Button({
label,
onClick,
disabled,
outline,
small,
icon: Icon,
isColor,
}: Props) {
return (
{Icon && (
)}
{label}
);
}
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 (
);
}
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 (
{children}
);
}
export default ClientOnly;
================================================
FILE: components/Container.tsx
================================================
"use client";
import React from "react";
type Props = {
children: React.ReactNode;
};
function Container({ children }: Props) {
return (
{children}
);
}
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 (
{showReset && (
router.push("/")}
/>
)}
);
}
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) => (
))
return (
{footerColumns}
{country}
);
}
export default Footer;
================================================
FILE: components/FooterColumn.tsx
================================================
"user client"
import { motion } from "framer-motion";
type Props = {
index: number;
data: Array;
};
function FooterColumn({ index, data }: Props) {
const columnItems = data.map((item, index) =>
index === 0
? {item}
: {item}
);
return (
{columnItems}
);
}
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 (
);
}
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 (
);
}
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(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 (
setDateRange(value)}
dateRange={dateRange}
onSubmit={onCreateReservation}
disabled={isLoading}
disabledDates={disableDates}
/>
);
}
export default ListingClient;
================================================
FILE: components/Loader.tsx
================================================
"use client";
import { motion } from "framer-motion";
import React from "react";
type Props = {};
function Loader({}: Props) {
return (
);
}
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 (
{locationValue ? (
<>
{center && (
)}
>
) : (
<>{center && }>
)}
);
}
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 (
What this place offers
{offersRowOne.map((item, index) => (
{item.label}
))}
{/* another row */}
{offersRowTwo.map((item, index) => (
{item.label}
))}
);
}
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 (
{`Where you'll sleep`}
Bedroom 3
1 queen bed, 1 single bed
);
}
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 (
<>
>
);
}
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 (
);
}
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 (
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"
}`}
>
{label}
);
}
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 (
);
}
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 (
onChange(value as CountrySelectValue)}
formatOptionLabel={(option: any) => (
{option.label},
{option.region}
)}
classNames={{
control: () => "p-3 border-2",
input: () => "text-lg",
option: () => "text-lg",
}}
theme={(theme) => ({
...theme,
borderRadius: 6,
colors: {
...theme.colors,
primary: "black",
primary25: "#ffe4e6",
},
})}
/>
);
}
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 (
{({ open }) => {
return (
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"
>
Click to upload
{value && (
)}
);
}}
);
}
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;
errors: FieldErrors;
};
function Input({
id,
label,
type = "text",
disabled,
formatPrice,
register,
required,
errors,
}: Props) {
return (
{formatPrice && (
)}
{label}
);
}
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) => {
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 (
router.push(`/listings/${data.id}`)}
className="col-span-1 cursor-pointer group"
>
{location?.region}, {location?.label}
{reservationDate || data.category}
${price} {!reservation &&
Night
}
{onAction && actionLabel && (
)}
);
}
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 (
);
}
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 (
<>
>
);
}
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 (
{guestCount} guests
{roomCount} rooms
{bathroomCount} bathrooms
{category && (
)}
aircover
Every booking includes free protection from Host cancellations,
listing inaccuracies, and other issues like trouble checking in.
Learn more
{description}
{`Where you’ll be`}
);
}
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 (
onChangeDate(value.selection)}
/>
);
}
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({
defaultValues: {
email: "",
password: "",
},
});
const onSubmit: SubmitHandler = (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 = (
);
const footerContent = (
signIn("google")}
/>
signIn("facebook")}
isColor
/>
{`Didn't have an Account?`}{" "}
Create an Account
);
return (
);
}
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 (
<>
{body}
{secondaryAction && secondaryActionLabel && (
)}
{footer}
>
);
}
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({
defaultValues: {
name: "",
email: "",
password: "",
},
});
const onSubmit: SubmitHandler = (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 = (
);
const footerContent = (
signIn("google")}
/>
signIn("facebook")}
isColor
/>
Already have an account?{" "}
Log in
);
return (
);
}
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({
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 = (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 = (
{categories.map((item, index) => (
setCustomValue("category", category)}
selected={category === item.label}
label={item.label}
icon={item.icon}
/>
))}
);
if (step === STEPS.LOCATION) {
bodyContent = (
setCustomValue("location", value)}
/>
);
}
if (step === STEPS.INFO) {
bodyContent = (
setCustomValue("guestCount", value)}
/>
setCustomValue("roomCount", value)}
/>
setCustomValue("bathroomCount", value)}
/>
);
}
if (step === STEPS.IMAGES) {
bodyContent = (
setCustomValue("imageSrc", value)}
value={imageSrc}
/>
);
}
if (step === STEPS.DESCRIPTION) {
bodyContent = (
);
}
if (step == STEPS.PRICE) {
bodyContent = (
);
}
return (
);
}
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();
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({
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 = (
setLocation(value as CountrySelectValue)}
/>
);
if (step === STEPS.DATE) {
bodyContent = (
setDateRange(value.selection)}
value={dateRange}
/>
);
}
if (step === STEPS.INFO) {
bodyContent = (
setGuestCount(value)}
value={guestCount}
title="Guests"
subtitle="How many guests are coming?"
/>
setRoomCount(value)}
value={roomCount}
title="Rooms"
subtitle="How many rooms do you need?"
/>
{
setBathroomCount(value);
}}
value={bathroomCount}
title="Bathrooms"
subtitle="How many bahtrooms do you need?"
/>
);
}
return (
);
}
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 (
{categories.map((items, index) => (
))}
);
}
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 (
router.push("/")}>
);
}
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 (
{label}
);
}
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 (
);
}
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 (
{locationLabel}
{durationLabel}
);
}
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 (
Airbnb your Home
{currentUser ? (
) : (
)}
{isOpen && (
{currentUser ? (
<>
router.push("/trips")}
label="My trips"
/>
router.push("/favorites")}
label="My favorites"
/>
router.push("/reservations")}
label="My reservations"
/>
router.push("/properties")}
label="My properties"
/>
signOut()} label="Logout" />
>
) : (
<>
>
)}
)}
);
}
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) => {
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((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((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((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((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 & {
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;
};