├── .eslintrc.json ├── .gitignore ├── 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 ├── components │ ├── Avatar.tsx │ ├── Button.tsx │ ├── CategoryBox.tsx │ ├── ClientOnly.tsx │ ├── Container.tsx │ ├── EmptyState.tsx │ ├── Heading.tsx │ ├── HeartButton.tsx │ ├── Loader.tsx │ ├── Map.tsx │ ├── inputs │ │ ├── Calendar.tsx │ │ ├── CategoryInput.tsx │ │ ├── Counter.tsx │ │ ├── CountrySelect.tsx │ │ ├── ImageUpload.tsx │ │ └── Input.tsx │ ├── listings │ │ ├── ListingCard.tsx │ │ ├── ListingCategory.tsx │ │ ├── ListingHead.tsx │ │ ├── ListingInfo.tsx │ │ └── ListingReservation.tsx │ ├── modals │ │ ├── LoginModal.tsx │ │ ├── Modal.tsx │ │ ├── RegisterModal.tsx │ │ ├── RentModal.tsx │ │ └── SearchModal.tsx │ └── navbar │ │ ├── Categories.tsx │ │ ├── Logo.tsx │ │ ├── MenuItem.tsx │ │ ├── Navbar.tsx │ │ ├── Search.tsx │ │ └── UserMenu.tsx ├── error.tsx ├── favicon.ico ├── favorites │ ├── FavoriteClient.tsx │ └── page.tsx ├── globals.css ├── hooks │ ├── useCountries.ts │ ├── useFavorite.ts │ ├── useLoginModal.ts │ ├── useRegisterModal.ts │ ├── useRentModal.ts │ └── useSearchModal.ts ├── layout.tsx ├── libs │ └── prismadb.ts ├── listings │ └── [listingId] │ │ ├── ListingClient.tsx │ │ └── page.tsx ├── loading.tsx ├── page.tsx ├── properties │ ├── PropertiesClient.tsx │ └── page.tsx ├── providers │ └── ToasterProvider.tsx ├── reservations │ ├── ReservationsClient.tsx │ └── page.tsx ├── trips │ ├── TripsClient.tsx │ └── page.tsx └── types │ └── index.ts ├── middleware.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages └── api │ └── auth │ └── [...nextauth].ts ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── images │ ├── logo-ftw.png │ ├── logo-yellow.png │ └── profile-10.jpg ├── next.svg ├── rent-app-screen-shot1.png └── vercel.svg ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Full Stack Rent Application with Next.js 14 App Router: React, Tailwind, Prisma, MongoDB, NextAuth 2023 2 | ![Screenshot](/public/rent-app-screen-shot1.png) 3 | 4 | ## Demo 5 | 6 | Check out the live demo here : https://renting-app-nextjs.vercel.app/. 7 | 8 | ## Overview 9 | 10 | This repository contains the source code for a Full Stack Rent Application built with Next.js 14, featuring the App Router, React, Tailwind, Prisma, MongoDB, and NextAuth. 11 | 12 | 13 | ## Features 14 | 15 | - Tailwind design 16 | - Tailwind animations and effects 17 | - Full responsiveness 18 | - Credential authentication 19 | - Google authentication 20 | - Github authentication 21 | - Image upload using Cloudinary CDN 22 | - Client form validation and handling using react-hook-form 23 | - Server error handling using react-toast 24 | - Calendars with react-date-range 25 | - Page loading state 26 | - Page empty state 27 | - Booking / Reservation system 28 | - Guest reservation cancellation 29 | - Owner reservation cancellation 30 | - Creation and deletion of properties 31 | - Pricing calculation 32 | - Advanced search algorithm by category, date range, map location, number of guests, rooms, and bathrooms 33 | - For example, filtering properties with reservations in the desired date range 34 | - Favorites system 35 | - Shareable URL filters 36 | - Share a URL with predefined filters for category, location, and date range 37 | - Demonstrates how to write POST and DELETE routes in route handlers (app/api) 38 | - Fetching data in server react components by directly accessing the database (WITHOUT API!) 39 | - Handling files like error.tsx and loading.tsx, new Next 14 templating files for unified loading and error handling 40 | - Managing relations between server and child components 41 | 42 | 43 | 44 | ## Setup .env file 45 | Create a .env file in the root directory and add the following configuration: 46 | ```bash 47 | DATABASE_URL= 48 | GOOGLE_CLIENT_ID= 49 | GOOGLE_CLIENT_SECRET= 50 | GITHUB_ID= 51 | GITHUB_SECRET= 52 | NEXTAUTH_SECRET= 53 | ``` 54 | ## Setup Prisma 55 | ```bash 56 | npx prisma db push 57 | ``` 58 | ## Start the app 59 | ```bash 60 | npm run dev 61 | ``` 62 | 63 | -------------------------------------------------------------------------------- /app/actions/getCurrentUser.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "next-auth/next" 2 | 3 | import { authOptions } from "@/pages/api/auth/[...nextauth]"; 4 | import prisma from "@/app/libs/prismadb"; 5 | 6 | export async function getSession() { 7 | return await getServerSession(authOptions) 8 | } 9 | 10 | export default async function getCurrentUser() { 11 | try { 12 | const session = await getSession(); 13 | 14 | if (!session?.user?.email) { 15 | return null; 16 | } 17 | 18 | const currentUser = await prisma.user.findUnique({ 19 | where: { 20 | email: session.user.email as string, 21 | } 22 | }); 23 | 24 | if (!currentUser) { 25 | return null; 26 | } 27 | 28 | return { 29 | ...currentUser, 30 | createdAt: currentUser.createdAt?.toISOString(), 31 | updatedAt: currentUser.updatedAt?.toISOString(), 32 | emailVerified: 33 | currentUser.emailVerified?.toISOString() || null, 34 | }; 35 | } catch (error: any) { 36 | return null; 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /app/actions/getFavoriteListings.ts: -------------------------------------------------------------------------------- 1 | 2 | import getCurrentUser from "@/app/actions/getCurrentUser"; 3 | import prisma from "@/app/libs/prismadb"; 4 | 5 | export default async function getFavoriteListings(){ 6 | try { 7 | const currentUser = await getCurrentUser(); 8 | if(!currentUser) { 9 | return []; 10 | }; 11 | const favorites = await prisma.listing.findMany({ 12 | where: { 13 | id:{ 14 | in:[...(currentUser.favoriteIds || [])] 15 | } 16 | } 17 | }) 18 | const SafeFavorites = favorites.map((favorite)=> ({ 19 | ...favorite, 20 | createdAt: favorite.createdAt.toISOString() 21 | })) 22 | 23 | return SafeFavorites; 24 | } catch (error:any) { 25 | throw new Error(error) 26 | } 27 | } -------------------------------------------------------------------------------- /app/actions/getListingById.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/app/libs/prismadb"; 2 | 3 | interface IParams { 4 | listingId?: string; 5 | } 6 | 7 | export default async function getListingById( 8 | params: IParams 9 | ) { 10 | try { 11 | const { listingId } = params; 12 | 13 | const listing = await prisma.listing.findUnique({ 14 | where: { 15 | id: listingId, 16 | }, 17 | include: { 18 | user: true 19 | } 20 | }); 21 | 22 | if (!listing) { 23 | return null; 24 | } 25 | 26 | return { 27 | ...listing, 28 | createdAt: listing.createdAt.toISOString(), 29 | user: { 30 | ...listing.user, 31 | createdAt: listing.user.createdAt?.toISOString(), 32 | updatedAt: listing.user.updatedAt?.toISOString(), 33 | emailVerified: 34 | listing.user.emailVerified?.toISOString () || null, 35 | 36 | } 37 | }; 38 | } catch (error: any) { 39 | throw new Error(error); 40 | } 41 | } -------------------------------------------------------------------------------- /app/actions/getListings.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import prisma from "@/app/libs/prismadb"; 4 | 5 | export interface IListingsParams { 6 | userId?: string; 7 | guestCount?: number; 8 | roomCount?: number; 9 | bathroomCount?: number; 10 | startDate?: string; 11 | endDate?: string; 12 | locationValue?: string; 13 | category?: string; 14 | } 15 | 16 | export default async function getListings(params: IListingsParams) { 17 | const { 18 | userId, 19 | roomCount, 20 | guestCount, 21 | bathroomCount, 22 | locationValue, 23 | startDate, 24 | endDate, 25 | category, 26 | } = params; 27 | 28 | let query: any = {}; 29 | 30 | if (userId) { 31 | query.userId = userId; 32 | } 33 | 34 | if (category) { 35 | query.category = category; 36 | } 37 | 38 | if (roomCount) { 39 | query.roomCount = { 40 | gte: +roomCount, 41 | }; 42 | } 43 | 44 | if (guestCount) { 45 | query.guestCount = { 46 | gte: +guestCount, 47 | }; 48 | } 49 | 50 | if (bathroomCount) { 51 | query.bathroomCount = { 52 | gte: +bathroomCount, 53 | }; 54 | } 55 | 56 | if (locationValue) { 57 | query.locationValue = locationValue; 58 | } 59 | 60 | if (startDate && endDate) { 61 | query.NOT = { 62 | reservations: { 63 | some: { 64 | OR: [ 65 | { 66 | endDate: { gte: startDate }, 67 | startDate: { lte: startDate }, 68 | }, 69 | { 70 | startDate: { lte: endDate }, 71 | endDate: { gte: endDate }, 72 | }, 73 | ], 74 | }, 75 | }, 76 | }; 77 | } 78 | 79 | const listings = await prisma.listing.findMany({ 80 | where: query, 81 | orderBy: { 82 | createdAt: "desc", 83 | }, 84 | }); 85 | 86 | const safeListings = listings.map((listing) => ({ 87 | ...listing, 88 | createdAt: listing.createdAt.toISOString(), 89 | })); 90 | 91 | return safeListings; 92 | } 93 | -------------------------------------------------------------------------------- /app/actions/getReservations.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/app/libs/prismadb"; 2 | 3 | interface IParams { 4 | listingId?: string; 5 | userId?: string; 6 | authorId?: string; 7 | } 8 | 9 | export default async function getReservations( 10 | params: IParams 11 | ) { 12 | try { 13 | const { listingId, userId, authorId } = params; 14 | 15 | const query: any = {}; 16 | 17 | if (listingId) { 18 | query.listingId = listingId; 19 | }; 20 | 21 | if (userId) { 22 | query.userId = userId; 23 | } 24 | 25 | if (authorId) { 26 | query.listing = { userId: authorId }; 27 | } 28 | 29 | const reservations = await prisma.reservation.findMany({ 30 | where: query, 31 | include: { 32 | listing: true 33 | }, 34 | orderBy: { 35 | createdAt: 'desc' 36 | } 37 | }); 38 | 39 | const safeReservations = reservations.map( 40 | (reservation) => ({ 41 | ...reservation, 42 | createdAt: reservation.createdAt.toISOString(), 43 | startDate: reservation.startDate.toISOString(), 44 | endDate: reservation.endDate.toISOString(), 45 | listing: { 46 | ...reservation.listing, 47 | createdAt: reservation.listing.createdAt.toISOString(), 48 | }, 49 | })); 50 | 51 | return safeReservations; 52 | } catch (error: any) { 53 | throw new Error(error); 54 | } 55 | } -------------------------------------------------------------------------------- /app/api/favorites/[listingId]/route.ts: -------------------------------------------------------------------------------- 1 | import getCurrentUser from "@/app/actions/getCurrentUser"; 2 | import prisma from "@/app/libs/prismadb"; 3 | import { NextResponse } from "next/server"; 4 | 5 | interface Iparams{ 6 | listingId?: string; 7 | } 8 | 9 | export async function POST( 10 | request:Request, 11 | {params}:{params:Iparams} 12 | ){ 13 | const currentUser = await getCurrentUser(); 14 | 15 | if(!currentUser){ 16 | return NextResponse.error() 17 | } 18 | 19 | const {listingId} = params; 20 | if(!listingId || typeof listingId !== "string"){ 21 | throw new Error('Invalid ID') 22 | } 23 | 24 | let favoriteIds = [...(currentUser.favoriteIds || [])] 25 | favoriteIds.push(listingId) 26 | const user = await prisma.user.update({ 27 | where:{ 28 | id:currentUser.id 29 | }, 30 | data:{ 31 | favoriteIds 32 | } 33 | }) 34 | 35 | return NextResponse.json(user) 36 | } 37 | 38 | export async function DELETE ( 39 | request :Request, 40 | {params}:{params:Iparams} 41 | ){ 42 | const currentUser = await getCurrentUser(); 43 | if(!currentUser){ 44 | return NextResponse.error() 45 | } 46 | const {listingId} = params ; 47 | if(!listingId || typeof listingId !== 'string') { 48 | throw new Error ('Invalid ID') 49 | } 50 | 51 | let favoriteIds = [...(currentUser.favoriteIds || [])] 52 | 53 | favoriteIds = favoriteIds.filter((id) =>id !== listingId) 54 | 55 | const user = await prisma.user.update({ 56 | where :{ 57 | id:currentUser.id 58 | }, 59 | data:{ 60 | favoriteIds 61 | } 62 | }) 63 | 64 | return NextResponse.json(user) 65 | } 66 | 67 | -------------------------------------------------------------------------------- /app/api/listings/[listingId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import prisma from "@/app/libs/prismadb" 4 | import getCurrentUser from "@/app/actions/getCurrentUser" 5 | 6 | interface IPArams { 7 | listingId?: string; 8 | } 9 | 10 | export async function DELETE( 11 | request:Request, 12 | {params}:{params:IPArams} 13 | ){ 14 | const currentUser = await getCurrentUser(); 15 | if(!currentUser){ 16 | return NextResponse.error() 17 | } 18 | 19 | const {listingId} = params; 20 | if((!listingId || typeof listingId !== "string")){ 21 | throw new Error('Invalid ID') 22 | } 23 | 24 | const listing = await prisma.listing.deleteMany({ 25 | where:{ 26 | id:listingId, 27 | userId:currentUser.id, 28 | } 29 | }) 30 | 31 | return NextResponse.json(listing) 32 | } -------------------------------------------------------------------------------- /app/api/listings/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import prisma from "@/app/libs/prismadb" 4 | import getCurrentUser from "@/app/actions/getCurrentUser" 5 | 6 | export async function POST(request: Request){ 7 | const currentUser = await getCurrentUser() 8 | 9 | if(!currentUser){ 10 | return NextResponse.error() 11 | } 12 | 13 | const body = await request.json(); 14 | const { 15 | title, 16 | description, 17 | imageSrc, 18 | category, 19 | roomCount, 20 | bathroomCount, 21 | guestCount, 22 | location, 23 | price 24 | } = body 25 | 26 | Object.keys(body).forEach((value:any) => { 27 | if(!body[value]){ 28 | NextResponse.error(); 29 | } 30 | }) 31 | 32 | const listing = await prisma.listing.create({ 33 | data:{ 34 | title, 35 | description, 36 | imageSrc, 37 | category, 38 | roomCount, 39 | bathroomCount, 40 | guestCount, 41 | locationValue:location.value, 42 | price:parseInt(price,10), 43 | userId:currentUser.id 44 | } 45 | }) 46 | 47 | return NextResponse.json(listing); 48 | } -------------------------------------------------------------------------------- /app/api/register/route.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import bcrypt from "bcrypt"; 3 | import prisma from "@/app/libs/prismadb"; 4 | import { NextResponse } from "next/server"; 5 | 6 | 7 | export async function POST(request: Request) { 8 | const body = await request.json(); 9 | const { email, name, password } = body; 10 | const hashedPassword = await bcrypt.hash(password, 12); 11 | 12 | const user = await prisma.user.create({ 13 | data: { 14 | email, 15 | name, 16 | hashedPassword, 17 | }, 18 | }); 19 | 20 | return NextResponse.json(user); 21 | } -------------------------------------------------------------------------------- /app/api/reservations/[reservationId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import getCurrentUser from "@/app/actions/getCurrentUser"; 4 | import prisma from "@/app/libs/prismadb"; 5 | 6 | interface IParams { 7 | reservationId?: string; 8 | } 9 | 10 | export async function DELETE( 11 | request: Request, 12 | { params }: { params: IParams }, 13 | // params: IParams 14 | ) { 15 | const currentUser = await getCurrentUser(); 16 | 17 | if (!currentUser) { 18 | return NextResponse.error(); 19 | } 20 | 21 | const { reservationId } = params; 22 | 23 | 24 | if (!reservationId || typeof reservationId !== 'string') { 25 | throw new Error('Invalid ID'); 26 | } 27 | 28 | const deletedListing = await prisma.reservation.deleteMany({ 29 | where: { 30 | id: reservationId, 31 | userId: currentUser.id 32 | } 33 | }); 34 | 35 | 36 | return NextResponse.json(deletedListing); 37 | } -------------------------------------------------------------------------------- /app/api/reservations/route.ts: -------------------------------------------------------------------------------- 1 | import {NextResponse} from "next/server" 2 | import prisma from "@/app/libs/prismadb"; 3 | import getCurrentUser from "@/app/actions/getCurrentUser"; 4 | 5 | export async function POST(request: Request) { 6 | const currentUser = await getCurrentUser(); 7 | if(!currentUser){ 8 | return NextResponse.error() 9 | } 10 | 11 | const body = await request.json(); 12 | 13 | const { 14 | listingId, 15 | startDate, 16 | endDate, 17 | totalPrice 18 | } = body ; 19 | 20 | if( !listingId || !startDate || !endDate || !totalPrice){ 21 | return NextResponse.error(); 22 | } 23 | 24 | const listingAndReservation = await prisma.listing.update({ 25 | where:{ 26 | id:listingId, 27 | }, 28 | data:{ 29 | reservations:{ 30 | create:{ 31 | userId: currentUser.id, 32 | startDate, 33 | endDate, 34 | totalPrice 35 | } 36 | } 37 | } 38 | }) 39 | 40 | return NextResponse.json(listingAndReservation) 41 | } 42 | -------------------------------------------------------------------------------- /app/components/Avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import Image from 'next/image' 3 | import React from 'react' 4 | 5 | 6 | interface AvatarProps { 7 | src:string |null| undefined 8 | } 9 | 10 | const Avatar:React.FC = ({src}) => { 11 | 12 | return ( 13 | Avatar 14 | ) 15 | } 16 | 17 | export default Avatar -------------------------------------------------------------------------------- /app/components/Button.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | "use client" 3 | import React from "react"; 4 | import { IconType } from "react-icons"; 5 | 6 | interface ButtonProps { 7 | label: string; 8 | onClick: (e: React.MouseEvent) => void; 9 | disabled?: boolean; 10 | outline?: boolean; 11 | small?: boolean; 12 | icon?: IconType; 13 | } 14 | const Button: React.FC = ({ 15 | label, 16 | onClick, 17 | disabled, 18 | outline, 19 | small, 20 | icon: Icon, 21 | }) => { 22 | return ( 23 | 44 | ); 45 | }; 46 | 47 | export default Button; 48 | -------------------------------------------------------------------------------- /app/components/CategoryBox.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | "use client" 3 | import { useRouter, useSearchParams } from "next/navigation"; 4 | import React, { useCallback } from "react"; 5 | import { IconType } from "react-icons"; 6 | import qs from "query-string"; 7 | interface CategoryBoxProps { 8 | icon: IconType; 9 | label: string; 10 | selected?: boolean; 11 | } 12 | const CategoryBox: React.FC = ({ 13 | icon: Icon, 14 | label, 15 | selected, 16 | }) => { 17 | const router = useRouter(); 18 | const params = useSearchParams(); 19 | 20 | const handleClick = useCallback(() => { 21 | let currentQuery = {}; 22 | if (params) { 23 | currentQuery = qs.parse(params.toString()); 24 | } 25 | 26 | const updatedQuery: any = { 27 | ...currentQuery, 28 | category: label, 29 | }; 30 | 31 | if (params?.get("category") === label) { 32 | delete updatedQuery.category; 33 | } 34 | 35 | const url = qs.stringifyUrl( 36 | { 37 | url: "/", 38 | query: updatedQuery, 39 | }, 40 | { 41 | skipNull: true, 42 | } 43 | ); 44 | 45 | router.push(url); 46 | }, [label, params, router]); 47 | return ( 48 |
64 | 65 |
{label}
66 |
67 | ); 68 | }; 69 | 70 | export default CategoryBox; 71 | -------------------------------------------------------------------------------- /app/components/ClientOnly.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState, useEffect } from 'react'; 4 | 5 | interface ClientOnlyProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | const ClientOnly: React.FC = ({ 10 | children 11 | }) => { 12 | const [hasMounted, setHasMounted] = useState(false); 13 | 14 | useEffect(() => { 15 | setHasMounted(true); 16 | }, []) 17 | 18 | if (!hasMounted) return null; 19 | 20 | return ( 21 | <> 22 | {children} 23 | 24 | ); 25 | }; 26 | 27 | export default ClientOnly; 28 | -------------------------------------------------------------------------------- /app/components/Container.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | interface ContainerProps { 4 | children: React.ReactNode 5 | }; 6 | 7 | const Container: React.FC = ({ children }) => { 8 | return ( 9 |
19 | {children} 20 |
21 | ); 22 | } 23 | 24 | export default Container; 25 | -------------------------------------------------------------------------------- /app/components/EmptyState.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from "next/navigation"; 4 | 5 | import Button from "./Button"; 6 | import Heading from "./Heading"; 7 | 8 | interface EmptyStateProps { 9 | title?: string; 10 | subtitle?: string; 11 | showReset?: boolean; 12 | } 13 | 14 | const EmptyState: React.FC = ({ 15 | title = "No exact matches", 16 | subtitle = "Try changing or removing some of your filters.", 17 | showReset 18 | }) => { 19 | const router = useRouter(); 20 | 21 | return ( 22 |
32 | 37 |
38 | {showReset && ( 39 |
46 |
47 | ); 48 | } 49 | 50 | export default EmptyState; -------------------------------------------------------------------------------- /app/components/Heading.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | interface HeadingProps { 3 | title:string; 4 | subtitle?:string; 5 | center?:boolean; 6 | } 7 | const Heading:React.FC = ({title,subtitle, center}) => { 8 | return ( 9 |
10 |
11 | {title} 12 |
13 |
14 | {subtitle} 15 |
16 |
17 | ) 18 | } 19 | 20 | export default Heading -------------------------------------------------------------------------------- /app/components/HeartButton.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | "use client"; 4 | import React from "react"; 5 | import { SafeUser } from "../types"; 6 | import { AiFillHeart, AiOutlineHeart } from "react-icons/ai"; 7 | import useFavorite from "../hooks/useFavorite"; 8 | 9 | interface HeartButtonProps { 10 | listingId: string; 11 | currentUser?: SafeUser | null; 12 | } 13 | const HeartButton: React.FC = ({ 14 | listingId, 15 | currentUser, 16 | }) => { 17 | const {hasFavorited, toggleFavorite} = useFavorite({ 18 | listingId, 19 | currentUser 20 | }) 21 | return ( 22 |
29 | 33 | 37 |
38 | ); 39 | }; 40 | 41 | export default HeartButton; 42 | -------------------------------------------------------------------------------- /app/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React from 'react' 3 | import { PuffLoader } from 'react-spinners' 4 | 5 | const Loader = () => { 6 | return ( 7 |
8 | 12 |
13 | ) 14 | } 15 | 16 | export default Loader -------------------------------------------------------------------------------- /app/components/Map.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import L from "leaflet" 3 | import {MapContainer,Marker,TileLayer} from "react-leaflet" 4 | import "leaflet/dist/leaflet.css" 5 | import markerIcon2x from "leaflet/dist/images/marker-icon-2x.png" 6 | import markerIcon from "leaflet/dist/images/marker-icon.png" 7 | import markerShadow from "leaflet/dist/images/marker-shadow.png" 8 | 9 | // @ts-ignore 10 | delete L.Icon.Default.prototype._getIconUrl; 11 | L.Icon.Default.mergeOptions({ 12 | iconUrl:markerIcon.src, 13 | iconRetinaUrl:markerIcon2x.src, 14 | shadowUrl:markerShadow.src, 15 | }) 16 | 17 | interface MapProps { 18 | center?:number[]; 19 | } 20 | 21 | 22 | const Map:React.FC = ({center}) => { 23 | return ( 24 | 30 | 33 | { 34 | center && ( 35 | 38 | ) 39 | } 40 | 41 | ) 42 | } 43 | 44 | export default Map -------------------------------------------------------------------------------- /app/components/inputs/Calendar.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | "use client"; 4 | import React from "react"; 5 | import { DateRange, Range,RangeKeyDict } from "react-date-range"; 6 | 7 | import "react-date-range/dist/styles.css" 8 | import "react-date-range/dist/theme/default.css" 9 | 10 | interface CalendarProps { 11 | value: Range; 12 | onChange: (value: RangeKeyDict) => void; 13 | disabledDates?: Date[]; 14 | } 15 | const Calendar: React.FC = ({ 16 | value, 17 | onChange, 18 | disabledDates, 19 | }) => { 20 | return ; 30 | }; 31 | 32 | export default Calendar; 33 | -------------------------------------------------------------------------------- /app/components/inputs/CategoryInput.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | "use client"; 4 | 5 | import { IconType } from "react-icons"; 6 | 7 | interface CategoryInputProps { 8 | icon: IconType; 9 | label: string; 10 | selected?: boolean; 11 | onClick: (value: string) => void; 12 | } 13 | const CategoryInput: React.FC = ({ 14 | icon: Icon, 15 | label, 16 | selected, 17 | onClick, 18 | }) => { 19 | return ( 20 |
onClick(label)} 22 | className={` 23 | rounded-xl 24 | border-2 25 | p-4 26 | flex 27 | flex-col 28 | gap-3 29 | hover:border-black 30 | transition 31 | cursor-pointer 32 | ${selected ? "border-black" : "border-neutral-200"} 33 | `}> 34 | 35 |
{label}
36 |
37 | ); 38 | }; 39 | 40 | export default CategoryInput; 41 | -------------------------------------------------------------------------------- /app/components/inputs/Counter.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | "use client"; 4 | 5 | import { useCallback } from "react"; 6 | import { AiOutlineMinus, AiOutlinePlus } from "react-icons/ai"; 7 | 8 | interface CounterProps { 9 | title: string; 10 | subtitle: string; 11 | value: number; 12 | onChange: (value: number) => void; 13 | } 14 | const Counter: React.FC = ({ 15 | title, 16 | subtitle, 17 | value, 18 | onChange, 19 | }) => { 20 | const onAdd = useCallback(() => { 21 | onChange(value + 1); 22 | }, [onChange, value]); 23 | 24 | const onReduce = useCallback(() => { 25 | if (value === 1) { 26 | return; 27 | } 28 | onChange(value - 1); 29 | }, [onChange, value]); 30 | 31 | return ( 32 |
33 |
34 |
{title}
35 |
{subtitle}
36 |
37 |
38 |
43 | 44 |
45 |
{value}
46 |
51 | 52 |
53 |
54 |
55 | ); 56 | }; 57 | 58 | export default Counter; 59 | -------------------------------------------------------------------------------- /app/components/inputs/CountrySelect.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Select from 'react-select' 4 | 5 | import useCountries from '@/app/hooks/useCountries'; 6 | 7 | export type CountrySelectValue = { 8 | flag: string; 9 | label: string; 10 | latlng: number[], 11 | region: string; 12 | value: string 13 | } 14 | 15 | interface CountrySelectProps { 16 | value?: CountrySelectValue; 17 | onChange: (value: CountrySelectValue) => void; 18 | } 19 | 20 | const CountrySelect: React.FC = ({ 21 | value, 22 | onChange 23 | }) => { 24 | const { getAll } = useCountries(); 25 | 26 | return ( 27 |
28 | 51 | 52 | 70 |
71 | ); 72 | }; 73 | 74 | export default Input; 75 | -------------------------------------------------------------------------------- /app/components/listings/ListingCard.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Image from "next/image"; 4 | import { useRouter } from "next/navigation"; 5 | import { useCallback, useMemo } from "react"; 6 | import { format } from 'date-fns'; 7 | 8 | import useCountries from "@/app/hooks/useCountries"; 9 | import { 10 | SafeListing, 11 | SafeReservation, 12 | SafeUser 13 | } from "@/app/types"; 14 | 15 | import HeartButton from "../HeartButton"; 16 | import Button from "../Button"; 17 | import ClientOnly from "../ClientOnly"; 18 | 19 | interface ListingCardProps { 20 | data: SafeListing; 21 | reservation?: SafeReservation; 22 | onAction?: (id: string) => void; 23 | disabled?: boolean; 24 | actionLabel?: string; 25 | actionId?: string; 26 | currentUser?: SafeUser | null 27 | }; 28 | 29 | const ListingCard: React.FC = ({ 30 | data, 31 | reservation, 32 | onAction, 33 | disabled, 34 | actionLabel, 35 | actionId = '', 36 | currentUser, 37 | }) => { 38 | const router = useRouter(); 39 | const { getByValue } = useCountries(); 40 | 41 | const location = getByValue(data.locationValue); 42 | 43 | const handleCancel = useCallback( 44 | (e: React.MouseEvent) => { 45 | e.stopPropagation(); 46 | 47 | if (disabled) { 48 | return; 49 | } 50 | 51 | onAction?.(actionId) 52 | }, [disabled, onAction, actionId]); 53 | 54 | const price = useMemo(() => { 55 | if (reservation) { 56 | return reservation.totalPrice; 57 | } 58 | 59 | return data.price; 60 | }, [reservation, data.price]); 61 | 62 | const reservationDate = useMemo(() => { 63 | if (!reservation) { 64 | return null; 65 | } 66 | 67 | const start = new Date(reservation.startDate); 68 | const end = new Date(reservation.endDate); 69 | 70 | return `${format(start, 'PP')} - ${format(end, 'PP')}`; 71 | }, [reservation]); 72 | 73 | return ( 74 |
router.push(`/listings/${data.id}`)} 76 | className="col-span-1 cursor-pointer group" 77 | > 78 |
79 |
88 | Listing 100 |
105 | 109 |
110 |
111 |
112 | {location?.region}, {location?.label} 113 |
114 |
115 | {reservationDate || data.category} 116 |
117 |
118 |
119 | $ {price} 120 |
121 | {!reservation && ( 122 |
night
123 | )} 124 |
125 | {onAction && actionLabel && ( 126 |
134 |
135 | ); 136 | } 137 | 138 | export default ListingCard; -------------------------------------------------------------------------------- /app/components/listings/ListingCategory.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from "react"; 4 | import { IconType } from "react-icons"; 5 | 6 | interface ListingCategoryProps { 7 | icon: IconType; 8 | label: string; 9 | description: string; 10 | } 11 | const ListingCategory: React.FC = ({ 12 | icon:Icon, 13 | label, 14 | description, 15 | }) => { 16 | return
17 |
18 | 19 |
20 |
{label}
21 |
{description}
22 |
23 |
24 |
25 | }; 26 | 27 | export default ListingCategory; 28 | -------------------------------------------------------------------------------- /app/components/listings/ListingHead.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | "use client "; 4 | import useCountries from "@/app/hooks/useCountries"; 5 | import { SafeUser } from "@/app/types"; 6 | import React from "react"; 7 | import Heading from "../Heading"; 8 | import Image from "next/image"; 9 | import HeartButton from "../HeartButton"; 10 | 11 | interface ListingHeadProps { 12 | title: string; 13 | locationValue: string; 14 | imageSrc: string; 15 | id: string; 16 | currentUser?: SafeUser | null; 17 | } 18 | export const ListingHead: React.FC = ({ 19 | title, 20 | locationValue, 21 | imageSrc, 22 | id, 23 | currentUser, 24 | }) => { 25 | const { getByValue } = useCountries(); 26 | const location = getByValue(locationValue); 27 | return ( 28 | <> 29 | 33 |
41 | image 42 |
43 | 47 |
48 |
49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /app/components/listings/ListingInfo.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import useCountries from '@/app/hooks/useCountries' 3 | import { SafeUser } from '@/app/types' 4 | import { latLng } from 'leaflet' 5 | import React from 'react' 6 | import { IconType } from 'react-icons' 7 | import Avatar from '../Avatar' 8 | import ListingCategory from './ListingCategory' 9 | import dynamic from 'next/dynamic' 10 | 11 | 12 | const Map = dynamic(() => import('../Map'),{ 13 | ssr:false 14 | }) 15 | 16 | interface ListingInfoProps { 17 | user:SafeUser, 18 | description:string, 19 | guestCount:number, 20 | roomCount:number, 21 | bathroomCount:number, 22 | category:{ 23 | icon:IconType, 24 | label:string, 25 | description:string, 26 | } | undefined 27 | locationValue:string 28 | } 29 | const ListingInfo:React.FC = ({ 30 | user, 31 | description, 32 | guestCount, 33 | roomCount, 34 | bathroomCount, 35 | category, 36 | locationValue, 37 | }) => { 38 | const {getByValue}=useCountries() 39 | const coordinates=getByValue(locationValue)?.latlng; 40 | return ( 41 |
42 |
43 |
44 |
Hosted by {user?.name}
45 | 46 |
47 |
52 |
{guestCount} guests
53 |
{roomCount} rooms
54 |
{bathroomCount} bathrooms
55 |
56 |
57 |
58 | {category && ( 59 | 64 | )} 65 |
66 |
{description}
67 |
68 | 69 |
70 | ) 71 | } 72 | 73 | export default ListingInfo -------------------------------------------------------------------------------- /app/components/listings/ListingReservation.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from "react"; 4 | import { Range } from "react-date-range"; 5 | import Calendar from "../inputs/Calendar"; 6 | import Button from "../Button"; 7 | 8 | interface ListingReservationProps { 9 | price: number; 10 | dateRange: Range; 11 | totalPrice: number; 12 | onChangeDate: (value: Range) => void; 13 | onSubmit: () => void; 14 | disabled?: boolean; 15 | disabledDates: Date[]; 16 | } 17 | const ListingReservation: React.FC = ({ 18 | price, 19 | dateRange, 20 | totalPrice, 21 | onChangeDate, 22 | onSubmit, 23 | disabled, 24 | disabledDates, 25 | }) => { 26 | return ( 27 |
28 |
29 |
$ {price}
30 |
night
31 |
32 |
33 | onChangeDate(value.selection)} 37 | /> 38 |
39 |
40 |
46 |
47 |
Total
48 |
$ {totalPrice}
49 |
50 |
51 | ); 52 | }; 53 | 54 | export default ListingReservation; 55 | -------------------------------------------------------------------------------- /app/components/modals/LoginModal.tsx: -------------------------------------------------------------------------------- 1 | // /** @format */ 2 | 3 | "use client"; 4 | 5 | import { useCallback, useState } from "react"; 6 | import { AiFillGithub } from "react-icons/ai"; 7 | import { FcGoogle } from "react-icons/fc"; 8 | import { toast } from "react-hot-toast"; 9 | import { FieldValues, SubmitHandler, useForm } from "react-hook-form"; 10 | import useLoginModal from "@/app/hooks/useLoginModal"; 11 | import Modal from "./Modal"; 12 | import Heading from "../Heading"; 13 | import Input from "../inputs/Input"; 14 | import Button from "../Button"; 15 | import useRegisterModal from "@/app/hooks/useRegisterModal"; 16 | import { signIn } from "next-auth/react"; 17 | import { useRouter } from "next/navigation"; 18 | 19 | const LoginModal = () => { 20 | const router = useRouter(); 21 | 22 | const registerModal = useRegisterModal(); 23 | const loginModal = useLoginModal(); 24 | 25 | const [isLoading, setIsLoading] = useState(false); 26 | const { 27 | register, 28 | handleSubmit, 29 | formState: { errors }, 30 | } = useForm({ 31 | defaultValues: { 32 | email: "", 33 | password: "", 34 | }, 35 | }); 36 | 37 | const onSubmit: SubmitHandler = (data) => { 38 | setIsLoading(true); 39 | signIn("credentials", { ...data, redirect: false }) 40 | .then((callback) => { 41 | console.log(callback); 42 | setIsLoading(false); 43 | if (callback?.ok) { 44 | toast.success("Logged in"); 45 | router.refresh(); 46 | loginModal.onClose(); 47 | } 48 | if (callback?.error) { 49 | toast.error(callback.error); 50 | } 51 | }) 52 | .catch((error) => { 53 | console.log(error); 54 | }); 55 | }; 56 | 57 | const Toggle = useCallback(() => { 58 | loginModal.onClose(); 59 | registerModal.onOpen(); 60 | }, [loginModal, registerModal]); 61 | 62 | const bodyContent = ( 63 |
64 | 65 | 73 | 82 |
83 | ); 84 | 85 | const footerContent = ( 86 |
87 |
88 |
115 | ); 116 | return ( 117 | 127 | ); 128 | }; 129 | 130 | export default LoginModal; 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /app/components/modals/Modal.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | "use client"; 3 | import React, { useCallback, useEffect, useState } from "react"; 4 | import { IoMdClose } from "react-icons/io"; 5 | import Button from "../Button"; 6 | 7 | interface ModalProps { 8 | isOpen?: boolean; 9 | onClose: () => void; 10 | onSubmit: () => void; 11 | title?: string; 12 | body?: React.ReactElement; 13 | footer?: React.ReactElement; 14 | actionLabel: string; 15 | disabled?: boolean; 16 | secondaryAction?: () => void; 17 | secondaryActionLabel?: string; 18 | } 19 | const Modal = ({ 20 | isOpen, 21 | onClose, 22 | onSubmit, 23 | title, 24 | body, 25 | footer, 26 | actionLabel, 27 | disabled, 28 | secondaryAction, 29 | secondaryActionLabel, 30 | }: ModalProps) => { 31 | const [showModal, setShowModal] = useState(isOpen); 32 | useEffect(() => { 33 | setShowModal(isOpen); 34 | }, [isOpen, onClose]); 35 | 36 | const handleClose = useCallback(() => { 37 | if (disabled) { 38 | return; 39 | } 40 | setShowModal(false); 41 | setTimeout(() => { 42 | onClose(); 43 | }, 300); 44 | }, [disabled, onClose]); 45 | 46 | const handleSubmit = useCallback(() => { 47 | if (disabled) { 48 | return; 49 | } 50 | onSubmit(); 51 | }, [disabled, onSubmit]); 52 | const handleSecondaryAction = useCallback(() => { 53 | if (disabled || !secondaryAction) { 54 | return; 55 | } 56 | secondaryAction(); 57 | }, [disabled, secondaryAction]); 58 | 59 | if (!isOpen) { 60 | return null; 61 | } 62 | 63 | return ( 64 | <> 65 |
66 |
67 | {/* CONTENT */} 68 |
72 |
73 | {/* HEADER */} 74 |
75 | 80 |
{title}
81 |
82 | {/* BODY */} 83 |
{body}
84 | {/* FOOTER */} 85 |
86 |
87 | {secondaryAction && secondaryActionLabel && ( 88 |
102 | {footer} 103 |
104 |
105 |
106 |
107 |
108 | 109 | ); 110 | }; 111 | 112 | export default Modal; 113 | -------------------------------------------------------------------------------- /app/components/modals/RegisterModal.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | "use client"; 4 | 5 | import axios from "axios"; 6 | import { FieldValues, SubmitHandler, useForm } from "react-hook-form"; 7 | import { useCallback, useState } from "react"; 8 | import { signIn } from "next-auth/react"; 9 | import { toast } from "react-hot-toast"; 10 | 11 | import useRegisterModal from "@/app/hooks/useRegisterModal"; 12 | import useLoginModal from "@/app/hooks/useLoginModal"; 13 | 14 | import { AiFillGithub } from "react-icons/ai"; 15 | import { FcGoogle } from "react-icons/fc"; 16 | 17 | import Modal from "./Modal"; 18 | import Heading from "../Heading"; 19 | import Input from "../inputs/Input"; 20 | import Button from "../Button"; 21 | 22 | const RegisterModal = () => { 23 | const registerModal = useRegisterModal(); 24 | const loginModal = useLoginModal(); 25 | 26 | const [isLoading, setIsLoading] = useState(false); 27 | const { 28 | register, 29 | handleSubmit, 30 | formState: { errors }, 31 | } = useForm({ 32 | defaultValues: { 33 | name: "", 34 | email: "", 35 | password: "", 36 | }, 37 | }); 38 | 39 | const onSubmit: SubmitHandler = (data) => { 40 | setIsLoading(true); 41 | axios 42 | .post("/api/register", data) 43 | .then(() => { 44 | registerModal.onClose(); 45 | loginModal.onOpen(); 46 | }) 47 | .catch((error) => { 48 | toast.error("something went wrong. Please try again"); 49 | }) 50 | .finally(() => { 51 | setIsLoading(false); 52 | }); 53 | }; 54 | 55 | const Toggle = useCallback(() => { 56 | registerModal.onClose(); 57 | loginModal.onOpen(); 58 | }, [loginModal, registerModal]); 59 | 60 | const bodyContent = ( 61 |
62 | 67 | 75 | 83 | 92 |
93 | ); 94 | 95 | const footerContent = ( 96 |
97 |
98 |
125 | ); 126 | return ( 127 | 137 | ); 138 | }; 139 | 140 | export default RegisterModal; 141 | -------------------------------------------------------------------------------- /app/components/modals/RentModal.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | "use client"; 3 | import React, { useMemo, useState } from "react"; 4 | import Modal from "./Modal"; 5 | import useRentModal from "@/app/hooks/useRentModal"; 6 | import Heading from "../Heading"; 7 | import { categories } from "../navbar/Categories"; 8 | import CategoryInput from "../inputs/CategoryInput"; 9 | import { FieldValues, SubmitHandler, useForm } from "react-hook-form"; 10 | import CountrySlect from "../inputs/CountrySelect"; 11 | import CountrySelect from "../inputs/CountrySelect"; 12 | import dynamic from "next/dynamic"; 13 | import Counter from "../inputs/Counter"; 14 | import ImageUpload from "../inputs/ImageUpload"; 15 | import Input from "../inputs/Input"; 16 | import axios from "axios"; 17 | import toast from "react-hot-toast"; 18 | import { useRouter } from "next/navigation"; 19 | // import Map from "../Map"; 20 | 21 | enum STEPS { 22 | CATEGORY = 0, 23 | LOCATION = 1, 24 | INFO = 2, 25 | IMAGES = 3, 26 | DESCRIPTION = 4, 27 | PRICE = 5, 28 | } 29 | const RentModal = () => { 30 | const router = useRouter(); 31 | const rentModal = useRentModal(); 32 | 33 | const [step, setStep] = useState(STEPS.CATEGORY); 34 | const [isLoading, setIsLoading] = useState(false); 35 | 36 | const { 37 | register, 38 | handleSubmit, 39 | setValue, 40 | watch, 41 | formState: { errors }, 42 | reset, 43 | } = useForm({ 44 | defaultValues: { 45 | category: "", 46 | location: null, 47 | guestCount: 1, 48 | roomCount: 1, 49 | bathroomCount: 1, 50 | imageSrc: "", 51 | price: 1, 52 | title: "", 53 | description: "", 54 | }, 55 | }); 56 | 57 | const category = watch("category"); 58 | const location = watch("location"); 59 | const guestCount = watch("guestCount"); 60 | const roomCount = watch("roomCount"); 61 | const bathroomCount = watch("bathroomCount"); 62 | const imageSrc = watch("imageSrc"); 63 | 64 | const Map = useMemo(() => dynamic(() => import('../Map'),{ 65 | ssr:false 66 | }),[location]) 67 | 68 | const setCustomValue = (id: string, value: any) => { 69 | setValue(id, value, { 70 | shouldValidate: true, 71 | shouldDirty: true, 72 | shouldTouch: true, 73 | }); 74 | }; 75 | 76 | const onBack = () => { 77 | setStep((value) => value - 1); 78 | }; 79 | const onNext = () => { 80 | setStep((value) => value + 1); 81 | }; 82 | const onSubmit: SubmitHandler = (data) =>{ 83 | if(step !== STEPS.PRICE){ 84 | return onNext() 85 | } 86 | setIsLoading(true); 87 | axios.post('/api/listings',data).then(() =>{ 88 | toast.success('Listing Created') 89 | router.refresh(); 90 | reset(); 91 | setStep(STEPS.CATEGORY); 92 | rentModal.onClose() 93 | }) 94 | .catch(() =>{ 95 | toast.error('Something went wrong.') 96 | }).finally(() => { 97 | setIsLoading(false) 98 | }) 99 | } 100 | 101 | const actionLabel = useMemo(() => { 102 | if (step === STEPS.PRICE) { 103 | return "Create"; 104 | } 105 | return "Next "; 106 | }, [step]); 107 | 108 | const secondaryActionLabel = useMemo(() => { 109 | if (step === STEPS.CATEGORY) { 110 | return undefined; 111 | } 112 | return "Back "; 113 | }, [step]); 114 | 115 | let bodyContent = ( 116 |
117 | 121 |
130 | {categories.map((item) => ( 131 |
132 | { 134 | setCustomValue("category", category); 135 | }} 136 | selected={category === item.label} 137 | label={item.label} 138 | icon={item.icon} 139 | /> 140 |
141 | ))} 142 |
143 |
144 | ); 145 | 146 | if (step === STEPS.LOCATION) { 147 | bodyContent = ( 148 |
149 | 153 | setCustomValue('location', value)} 156 | /> 157 | 158 |
159 | ); 160 | } 161 | if (step === STEPS.INFO) { 162 | bodyContent = ( 163 |
164 | 168 | setCustomValue('guestCount',value)} 173 | /> 174 |
175 | setCustomValue('roomCount',value)} 180 | /> 181 |
182 | setCustomValue('bathroomCount',value)} 187 | /> 188 |
189 | ); 190 | } 191 | if (step === STEPS.IMAGES) { 192 | bodyContent = ( 193 |
194 | 198 | setCustomValue('imageSrc',value)} 201 | /> 202 |
203 | ); 204 | } 205 | if (step === STEPS.DESCRIPTION) { 206 | bodyContent = ( 207 |
208 | 212 | 220 |
221 | 229 |
230 | ); 231 | } 232 | if (step === STEPS.PRICE) { 233 | bodyContent = ( 234 |
235 | 239 | 249 | 250 |
251 | ); 252 | } 253 | return ( 254 | 264 | ); 265 | }; 266 | 267 | export default RentModal; 268 | -------------------------------------------------------------------------------- /app/components/modals/SearchModal.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import qs from 'query-string'; 4 | import dynamic from 'next/dynamic' 5 | import { useCallback, useMemo, useState } from "react"; 6 | import { Range } from 'react-date-range'; 7 | import { formatISO } from 'date-fns'; 8 | import { useRouter, useSearchParams } from 'next/navigation'; 9 | 10 | import useSearchModal from "@/app/hooks/useSearchModal"; 11 | 12 | import Modal from "./Modal"; 13 | import Calendar from "../inputs/Calendar"; 14 | import Counter from "../inputs/Counter"; 15 | import CountrySelect, { 16 | CountrySelectValue 17 | } from "../inputs/CountrySelect"; 18 | import Heading from '../Heading'; 19 | 20 | enum STEPS { 21 | LOCATION = 0, 22 | DATE = 1, 23 | INFO = 2, 24 | } 25 | 26 | const SearchModal = () => { 27 | const router = useRouter(); 28 | const searchModal = useSearchModal(); 29 | const params = useSearchParams(); 30 | 31 | const [step, setStep] = useState(STEPS.LOCATION); 32 | 33 | const [location, setLocation] = useState(); 34 | const [guestCount, setGuestCount] = useState(1); 35 | const [roomCount, setRoomCount] = useState(1); 36 | const [bathroomCount, setBathroomCount] = useState(1); 37 | const [dateRange, setDateRange] = useState({ 38 | startDate: new Date(), 39 | endDate: new Date(), 40 | key: 'selection' 41 | }); 42 | 43 | const Map = useMemo(() => dynamic(() => import('../Map'), { 44 | ssr: false 45 | }), [location]); 46 | 47 | const onBack = useCallback(() => { 48 | setStep((value) => value - 1); 49 | }, []); 50 | 51 | const onNext = useCallback(() => { 52 | setStep((value) => value + 1); 53 | }, []); 54 | 55 | const onSubmit = useCallback(async () => { 56 | if (step !== STEPS.INFO) { 57 | return onNext(); 58 | } 59 | 60 | let currentQuery = {}; 61 | 62 | if (params) { 63 | currentQuery = qs.parse(params.toString()) 64 | } 65 | 66 | const updatedQuery: any = { 67 | ...currentQuery, 68 | locationValue: location?.value, 69 | guestCount, 70 | roomCount, 71 | bathroomCount 72 | }; 73 | 74 | if (dateRange.startDate) { 75 | updatedQuery.startDate = formatISO(dateRange.startDate); 76 | } 77 | 78 | if (dateRange.endDate) { 79 | updatedQuery.endDate = formatISO(dateRange.endDate); 80 | } 81 | 82 | const url = qs.stringifyUrl({ 83 | url: '/', 84 | query: updatedQuery, 85 | }, { skipNull: true }); 86 | 87 | setStep(STEPS.LOCATION); 88 | searchModal.onClose(); 89 | router.push(url); 90 | }, 91 | [ 92 | step, 93 | searchModal, 94 | location, 95 | router, 96 | guestCount, 97 | roomCount, 98 | dateRange, 99 | onNext, 100 | bathroomCount, 101 | params 102 | ]); 103 | 104 | const actionLabel = useMemo(() => { 105 | if (step === STEPS.INFO) { 106 | return 'Search' 107 | } 108 | 109 | return 'Next' 110 | }, [step]); 111 | 112 | const secondaryActionLabel = useMemo(() => { 113 | if (step === STEPS.LOCATION) { 114 | return undefined 115 | } 116 | 117 | return 'Back' 118 | }, [step]); 119 | 120 | let bodyContent = ( 121 |
122 | 126 | 129 | setLocation(value as CountrySelectValue)} 130 | /> 131 |
132 | 133 |
134 | ) 135 | 136 | if (step === STEPS.DATE) { 137 | bodyContent = ( 138 |
139 | 143 | setDateRange(value.selection)} 145 | value={dateRange} 146 | /> 147 |
148 | ) 149 | } 150 | 151 | if (step === STEPS.INFO) { 152 | bodyContent = ( 153 |
154 | 158 | setGuestCount(value)} 160 | value={guestCount} 161 | title="Guests" 162 | subtitle="How many guests are coming?" 163 | /> 164 |
165 | setRoomCount(value)} 167 | value={roomCount} 168 | title="Rooms" 169 | subtitle="How many rooms do you need?" 170 | /> 171 |
172 | { 174 | setBathroomCount(value) 175 | }} 176 | value={bathroomCount} 177 | title="Bathrooms" 178 | subtitle="How many bathrooms do you need?" 179 | /> 180 |
181 | ) 182 | } 183 | 184 | return ( 185 | 195 | ); 196 | } 197 | 198 | export default SearchModal; -------------------------------------------------------------------------------- /app/components/navbar/Categories.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | "use client" 3 | import React from "react"; 4 | import Container from "../Container"; 5 | 6 | import { TbBeach, TbMountain, TbPool } from 'react-icons/tb'; 7 | import { 8 | GiBarn, 9 | GiBoatFishing, 10 | GiCactus, 11 | GiCastle, 12 | GiCaveEntrance, 13 | GiForestCamp, 14 | GiIsland, 15 | GiWindmill 16 | } from 'react-icons/gi'; 17 | import { FaSkiing } from 'react-icons/fa'; 18 | import { BsSnow } from 'react-icons/bs'; 19 | import { IoDiamond } from 'react-icons/io5'; 20 | import { MdOutlineVilla } from 'react-icons/md'; 21 | 22 | import CategoryBox from "../CategoryBox"; 23 | import { usePathname, useSearchParams } from "next/navigation"; 24 | 25 | export const categories = [ 26 | { 27 | label: "Beach", 28 | icon: TbBeach, 29 | description: "This property is close to the beach", 30 | }, 31 | { 32 | label: "Windmills", 33 | icon: GiWindmill, 34 | description: "This property has windmills", 35 | }, 36 | { 37 | label: "Modern", 38 | icon: MdOutlineVilla, 39 | description: "This property is modern ", 40 | }, 41 | { 42 | label: "Countryside", 43 | icon: TbMountain, 44 | description: "This property is the countryside ", 45 | }, 46 | { 47 | label: "Pools", 48 | icon: TbPool, 49 | description: "This property has a pool ", 50 | }, 51 | { 52 | label: "Islands", 53 | icon: GiIsland, 54 | description: "This property is on an Island ", 55 | }, 56 | { 57 | label: "Lake", 58 | icon: GiBoatFishing, 59 | description: "This property is close to a lake ", 60 | }, 61 | { 62 | label: "Skiing", 63 | icon: FaSkiing, 64 | description: "This property has skiing activities ", 65 | }, 66 | { 67 | label: "Castles", 68 | icon: GiCastle, 69 | description: "This property in a castle ", 70 | }, 71 | { 72 | label: "Camping", 73 | icon: GiForestCamp, 74 | description: "This property has camping activities ", 75 | }, 76 | { 77 | label: 'Arctic', 78 | icon: BsSnow, 79 | description: 'This property is in arctic environment!' 80 | }, 81 | { 82 | label: 'Desert', 83 | icon: GiCactus, 84 | description: 'This property is in the desert!' 85 | }, 86 | { 87 | label: 'Barns', 88 | icon: GiBarn, 89 | description: 'This property is in a barn!' 90 | }, 91 | { 92 | label: 'Lux', 93 | icon: IoDiamond, 94 | description: 'This property is brand new and luxurious!' 95 | } 96 | ]; 97 | const Categories = () => { 98 | const params = useSearchParams(); 99 | const category = params?.get('category'); 100 | const pathname = usePathname() 101 | 102 | const isMAinPage = pathname === '/' 103 | 104 | if(!isMAinPage){ 105 | return null 106 | } 107 | return ( 108 | 109 |
110 | { 111 | categories.map((item) =>( 112 | 118 | )) 119 | } 120 |
121 |
122 | ); 123 | }; 124 | 125 | export default Categories; 126 | -------------------------------------------------------------------------------- /app/components/navbar/Logo.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | "use client"; 4 | import Image from "next/image"; 5 | import { useRouter } from "next/navigation"; 6 | import React from "react"; 7 | 8 | const Logo = () => { 9 | const router = useRouter(); 10 | 11 | return ( 12 | router.push('/')} 14 | // src='/images/logo-ftw.png' 15 | src='/images/logo-yellow.png' 16 | alt='logo' 17 | height='110' 18 | width='110' 19 | className='hidden md:block cursor-pointer hover:scale-105' 20 | /> 21 | ); 22 | }; 23 | export default Logo; -------------------------------------------------------------------------------- /app/components/navbar/MenuItem.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | interface MenuItemsProps { 4 | onClick: () => void; 5 | label: string; 6 | } 7 | const MenuItem:React.FC = ({onClick, 8 | label}) => { 9 | return ( 10 |
{label}
11 | ) 12 | } 13 | 14 | export default MenuItem -------------------------------------------------------------------------------- /app/components/navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | "use client"; 3 | import React from "react"; 4 | import Container from "../Container"; 5 | import Logo from "./Logo"; 6 | import Search from "./Search"; 7 | import UserMenu from "./UserMenu"; 8 | import { SafeUser } from "@/app/types"; 9 | import Categories from "./Categories"; 10 | 11 | 12 | interface NavbarProps { 13 | currentUser?:SafeUser | null 14 | } 15 | const Navbar:React.FC = ({currentUser}) => { 16 | console.log(currentUser); 17 | 18 | return ( 19 |
20 |
21 | 22 |
23 | 24 | 25 | 26 |
27 |
28 |
29 | 30 |
31 | ); 32 | } 33 | 34 | export default Navbar; 35 | -------------------------------------------------------------------------------- /app/components/navbar/Search.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | "use client"; 3 | import useCountries from "@/app/hooks/useCountries"; 4 | import useSearchModal from "@/app/hooks/useSearchModal"; 5 | import { differenceInDays } from "date-fns"; 6 | import { useSearchParams } from "next/navigation"; 7 | import React, { useMemo } from "react"; 8 | import { BiSearch } from "react-icons/bi"; 9 | 10 | const Search = () => { 11 | const searchModal = useSearchModal(); 12 | const params = useSearchParams(); 13 | const { getByValue } = useCountries(); 14 | 15 | const locationValue = params?.get("locationValue"); 16 | const startDate = params?.get("startDate"); 17 | const endDate = params?.get("endDate"); 18 | const guestCount = params?.get("guestCount"); 19 | 20 | const locationLabel = useMemo(() => { 21 | if (locationValue) { 22 | return getByValue(locationValue as string)?.label; 23 | } 24 | return "Anywhere"; 25 | }, [getByValue, locationValue]); 26 | 27 | const durationLAbel = useMemo(() => { 28 | if (startDate && endDate) { 29 | const start = new Date(startDate as string); 30 | const end = new Date(endDate as string); 31 | let diff = differenceInDays(end, start); 32 | 33 | if (diff === 0) { 34 | diff = 1; 35 | } 36 | 37 | return `${diff} Days` 38 | } 39 | 40 | return 'Any week' 41 | }, [startDate,endDate]); 42 | 43 | 44 | const guestLabel = useMemo(() => { 45 | if (guestCount) { 46 | return `${guestCount} Guests`; 47 | } 48 | return "Add Guests"; 49 | }, [guestCount]); 50 | 51 | 52 | return ( 53 |
56 |
57 |
{locationLabel}
58 |
59 | {durationLAbel} 60 |
61 |
62 |
{guestLabel}
63 |
64 | 65 |
66 |
67 |
68 |
69 | ); 70 | }; 71 | 72 | export default Search; 73 | -------------------------------------------------------------------------------- /app/components/navbar/UserMenu.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useCallback, useState } from "react"; 4 | import { AiOutlineMenu } from "react-icons/ai"; 5 | import { signOut } from "next-auth/react"; 6 | import { useRouter } from "next/navigation"; 7 | 8 | import useLoginModal from "@/app/hooks/useLoginModal"; 9 | import useRegisterModal from "@/app/hooks/useRegisterModal"; 10 | import useRentModal from "@/app/hooks/useRentModal"; 11 | import { SafeUser } from "@/app/types"; 12 | 13 | import MenuItem from "./MenuItem"; 14 | import Avatar from "../Avatar"; 15 | 16 | interface UserMenuProps { 17 | currentUser?: SafeUser | null 18 | } 19 | 20 | const UserMenu: React.FC = ({ 21 | currentUser 22 | }) => { 23 | const router = useRouter(); 24 | 25 | const loginModal = useLoginModal(); 26 | const registerModal = useRegisterModal(); 27 | const rentModal = useRentModal(); 28 | 29 | const [isOpen, setIsOpen] = useState(false); 30 | 31 | const toggleOpen = useCallback(() => { 32 | setIsOpen((value) => !value); 33 | }, []); 34 | 35 | const onRent = useCallback(() => { 36 | if (!currentUser) { 37 | return loginModal.onOpen(); 38 | } 39 | 40 | rentModal.onOpen(); 41 | }, [loginModal, rentModal, currentUser]); 42 | 43 | return ( 44 |
45 |
46 |
61 | {currentUser?.name} 62 |
63 |
81 | 82 |
83 | 84 |
85 |
86 |
87 | {isOpen && ( 88 |
106 |
107 | {currentUser ? ( 108 | <> 109 | router.push('/trips')} 112 | /> 113 | router.push('/favorites')} 116 | /> 117 | router.push('/reservations')} 120 | /> 121 | router.push('/properties')} 124 | /> 125 | 129 |
130 | signOut()} 133 | /> 134 | 135 | ) : ( 136 | <> 137 | 141 | 145 | 146 | )} 147 |
148 |
149 | )} 150 |
151 | ); 152 | } 153 | 154 | export default UserMenu; -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import {useEffect} from "react" 4 | import EmptyState from "./components/EmptyState" 5 | 6 | interface ErrorStateProps{ 7 | error:Error 8 | } 9 | 10 | const ErrorState:React.FC=({error})=>{ 11 | useEffect(() => { 12 | console.error(error); 13 | 14 | },[error]) 15 | 16 | return( 17 | 21 | ) 22 | } 23 | 24 | export default ErrorState -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiraadev/rent-application-nextjs/807d6dd342b2a9fcd3e8bc666c8fa1b971b15b9c/app/favicon.ico -------------------------------------------------------------------------------- /app/favorites/FavoriteClient.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from "react"; 4 | import { SafeListing, SafeUser } from "../types"; 5 | import Container from "../components/Container"; 6 | import Heading from "../components/Heading"; 7 | import ListingCard from "../components/listings/ListingCard"; 8 | 9 | interface FavoriteClientProps { 10 | listings: SafeListing[]; 11 | currentUser: SafeUser | null; 12 | } 13 | const FavoriteClient: React.FC = ({ 14 | listings, 15 | currentUser, 16 | }) => { 17 | return 18 | 22 |
33 | {listings.map((listing) =>( 34 | 39 | ))} 40 |
41 |
; 42 | }; 43 | 44 | export default FavoriteClient; 45 | -------------------------------------------------------------------------------- /app/favorites/page.tsx: -------------------------------------------------------------------------------- 1 | import getCurrentUser from "@/app/actions/getCurrentUser"; 2 | import getListingById from "@/app/actions/getListingById"; 3 | import ClientOnly from "@/app/components/ClientOnly"; 4 | import EmptyState from "@/app/components/EmptyState"; 5 | import getReservations from "@/app/actions/getReservations"; 6 | import getFavoriteListings from "../actions/getFavoriteListings"; 7 | import FavoriteClient from "./FavoriteClient"; 8 | 9 | const ListingPage = async () => { 10 | 11 | const listings = await getFavoriteListings(); 12 | const currentUser = await getCurrentUser(); 13 | 14 | if(listings.length === 0){ 15 | return( 16 | 17 | 21 | 22 | ) 23 | } 24 | 25 | return ( 26 | 27 | 31 | 32 | ) 33 | } 34 | 35 | export default ListingPage; -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | html, 8 | body, 9 | :root { 10 | height: 100%; 11 | background-color: rgb(30 41 59); 12 | } 13 | 14 | .leaflet-bottom, 15 | .leaflet-control, 16 | .leaflet-pane, 17 | .leaflet-top { 18 | z-index: 0 !important; 19 | } 20 | 21 | .rdrMonth { 22 | width: 100% !important; 23 | } 24 | 25 | .rdrCalendarWrapper { 26 | font-size: 16px !important; 27 | width: 100% !important; 28 | } 29 | -------------------------------------------------------------------------------- /app/hooks/useCountries.ts: -------------------------------------------------------------------------------- 1 | import countries from 'world-countries'; 2 | 3 | const formattedCountries = countries.map((country) => ({ 4 | value: country.cca2, 5 | label: country.name.common, 6 | flag: country.flag, 7 | latlng: country.latlng, 8 | region: country.region, 9 | })); 10 | 11 | const useCountries = () => { 12 | const getAll = () => formattedCountries; 13 | 14 | const getByValue = (value: string) => { 15 | return formattedCountries.find((item) => item.value === value); 16 | } 17 | 18 | return { 19 | getAll, 20 | getByValue 21 | } 22 | }; 23 | 24 | export default useCountries; -------------------------------------------------------------------------------- /app/hooks/useFavorite.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import {useRouter} from "next/navigation"; 3 | import { useCallback, useMemo } from "react"; 4 | import {toast} from "react-hot-toast"; 5 | 6 | import { SafeUser } from "../types"; 7 | 8 | import useLoginModal from "./useLoginModal" 9 | 10 | interface IUseFavorite { 11 | listingId:string; 12 | currentUser?:SafeUser | null; 13 | } 14 | 15 | const useFavorite =({ 16 | listingId, 17 | currentUser 18 | }:IUseFavorite) => { 19 | const router = useRouter() 20 | const loginModal = useLoginModal() 21 | 22 | const hasFavorited = useMemo(() => { 23 | const list = currentUser?.favoriteIds || []; 24 | return list.includes(listingId); 25 | },[currentUser,listingId]) 26 | 27 | const toggleFavorite = useCallback(async(e:React.MouseEvent) => { 28 | e.stopPropagation(); 29 | if(!currentUser){ 30 | return loginModal.onOpen() 31 | } 32 | 33 | try { 34 | let request; 35 | if(hasFavorited){ 36 | request = () => axios.delete(`/api/favorites/${listingId}`) 37 | } else { 38 | request = () => axios.post(`/api/favorites/${listingId}`) 39 | } 40 | 41 | await request(); 42 | router.refresh(); 43 | toast.success('Success') 44 | } catch (error) { 45 | toast.error('Something went wrong') 46 | } 47 | }, 48 | [ 49 | currentUser, 50 | hasFavorited, 51 | listingId, 52 | loginModal, 53 | router 54 | ]) 55 | 56 | return { 57 | hasFavorited, 58 | toggleFavorite 59 | } 60 | } 61 | export default useFavorite; -------------------------------------------------------------------------------- /app/hooks/useLoginModal.ts: -------------------------------------------------------------------------------- 1 | import {create} from "zustand"; 2 | 3 | interface LoginModalStore { 4 | isOpen:boolean; 5 | onOpen:() => void; 6 | onClose:() => void; 7 | } 8 | 9 | const useLoginModal = create((set) => ({ 10 | isOpen:false, 11 | onOpen: () => set({isOpen:true}), 12 | onClose: () => set({isOpen:false}), 13 | 14 | })) 15 | export default useLoginModal -------------------------------------------------------------------------------- /app/hooks/useRegisterModal.ts: -------------------------------------------------------------------------------- 1 | import {create} from "zustand"; 2 | 3 | interface RegisterModalStore { 4 | isOpen:boolean; 5 | onOpen:() => void; 6 | onClose:() => void; 7 | } 8 | 9 | const useRegisterModal = create((set) => ({ 10 | isOpen:false, 11 | onOpen: () => set({isOpen:true}), 12 | onClose: () => set({isOpen:false}), 13 | 14 | })) 15 | export default useRegisterModal -------------------------------------------------------------------------------- /app/hooks/useRentModal.ts: -------------------------------------------------------------------------------- 1 | import {create} from "zustand"; 2 | 3 | interface RentModalStore { 4 | isOpen:boolean; 5 | onOpen:() => void; 6 | onClose:() => void; 7 | } 8 | 9 | const useRentModal = create((set) => ({ 10 | isOpen:false, 11 | onOpen: () => set({isOpen:true}), 12 | onClose: () => set({isOpen:false}), 13 | 14 | })) 15 | export default useRentModal -------------------------------------------------------------------------------- /app/hooks/useSearchModal.ts: -------------------------------------------------------------------------------- 1 | import {create} from "zustand"; 2 | 3 | interface SearchModalStore { 4 | isOpen:boolean; 5 | onOpen:() => void; 6 | onClose:() => void; 7 | } 8 | 9 | const useSearchModal = create((set) => ({ 10 | isOpen:false, 11 | onOpen: () => set({isOpen:true}), 12 | onClose: () => set({isOpen:false}), 13 | 14 | })) 15 | export default useSearchModal -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Nunito } from 'next/font/google' 2 | 3 | import Navbar from '@/app/components/navbar/Navbar'; 4 | import LoginModal from '@/app/components/modals/LoginModal'; 5 | import RegisterModal from '@/app/components/modals/RegisterModal'; 6 | import SearchModal from '@/app/components/modals/SearchModal'; 7 | import RentModal from '@/app/components/modals/RentModal'; 8 | 9 | import ToasterProvider from '@/app/providers/ToasterProvider'; 10 | 11 | import './globals.css' 12 | import ClientOnly from './components/ClientOnly'; 13 | import getCurrentUser from './actions/getCurrentUser'; 14 | 15 | export const metadata = { 16 | title: 'Amira FTW', 17 | description: 'Rent Application', 18 | } 19 | 20 | const font = Nunito({ 21 | subsets: ['latin'], 22 | }); 23 | 24 | export default async function RootLayout({ 25 | children, 26 | }: { 27 | children: React.ReactNode 28 | }) { 29 | const currentUser = await getCurrentUser(); 30 | 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | {children} 44 |
45 | 46 | 47 | ) 48 | } -------------------------------------------------------------------------------- /app/libs/prismadb.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | declare global { 4 | var prisma : PrismaClient | undefined; 5 | } 6 | 7 | const client = globalThis.prisma || new PrismaClient(); 8 | if(process.env.NODE_ENV !== 'production'){ 9 | globalThis.prisma = client; 10 | } 11 | 12 | export default client; -------------------------------------------------------------------------------- /app/listings/[listingId]/ListingClient.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | "use client"; 4 | import React, { useCallback, useEffect, useMemo, useState } from "react"; 5 | import { SafeListing, SafeReservation, SafeUser } from "@/app/types"; 6 | import { categories } from "@/app/components/navbar/Categories"; 7 | import Container from "@/app/components/Container"; 8 | import { ListingHead } from "@/app/components/listings/ListingHead"; 9 | import ListingInfo from "@/app/components/listings/ListingInfo"; 10 | import useLoginModal from "@/app/hooks/useLoginModal"; 11 | import { useRouter } from "next/navigation"; 12 | import { differenceInCalendarDays, eachDayOfInterval } from "date-fns"; 13 | import axios from "axios"; 14 | import toast from "react-hot-toast"; 15 | import ListingReservation from "@/app/components/listings/ListingReservation"; 16 | import { Range } from "react-date-range"; 17 | 18 | const initialDateRange = { 19 | startDate: new Date(), 20 | endDate: new Date(), 21 | key: "selection", 22 | }; 23 | 24 | interface ListingClientProps { 25 | reservations?: SafeReservation[]; 26 | listing: SafeListing & { 27 | user: SafeUser; 28 | }; 29 | currentUser?: SafeUser | null; 30 | } 31 | const ListingClient: React.FC = ({ 32 | listing, 33 | reservations = [], 34 | currentUser, 35 | }) => { 36 | const loginModal = useLoginModal(); 37 | const router = useRouter(); 38 | 39 | const disabledDates = useMemo(() => { 40 | let dates: Date[] = []; 41 | reservations.forEach((reservation)=>{ 42 | const range = eachDayOfInterval({ 43 | start:new Date(reservation.startDate), 44 | end:new Date(reservation.endDate), 45 | }) 46 | dates =[...dates,...range] 47 | }) 48 | return dates; 49 | }, [reservations]); 50 | 51 | const [isLoading,setIsLoading] = useState(false) 52 | const [totalPrice,setTotalPrice] = useState(listing.price) 53 | const [dateRange,setDateRange] = useState(initialDateRange) 54 | 55 | const onCreateReservation = useCallback( 56 | () => { 57 | if(!currentUser){ 58 | return loginModal.onOpen(); 59 | } 60 | setIsLoading(true) 61 | axios.post('/api/reservations',{ 62 | totalPrice, 63 | startDate:dateRange.startDate, 64 | endDate:dateRange.endDate, 65 | listingId:listing?.id 66 | }) 67 | .then(() =>{ 68 | toast.success('Listing reserved!'); 69 | setDateRange(initialDateRange); 70 | router.push('/trips') 71 | }) 72 | .catch(() => { 73 | toast.error('Something went wrong') 74 | }) 75 | .finally(() =>{ 76 | setIsLoading(false) 77 | }) 78 | }, 79 | [totalPrice,dateRange,listing?.id,router,currentUser,loginModal], 80 | ) 81 | 82 | useEffect(() => { 83 | if(dateRange.startDate && dateRange.endDate){ 84 | const dayCount = differenceInCalendarDays(dateRange.endDate,dateRange.startDate) 85 | 86 | if(dayCount && listing.price){ 87 | setTotalPrice(dayCount * listing.price) 88 | } else { 89 | setTotalPrice(listing.price) 90 | } 91 | } 92 | 93 | 94 | }, [dateRange,listing.price]) 95 | 96 | 97 | const category = useMemo(() => { 98 | return categories.find((item) => item.label === listing.category); 99 | }, [listing.category]); 100 | 101 | return ( 102 | 103 |
104 |
105 | 112 |
113 | 122 |
125 | setDateRange(value)} 129 | dateRange={dateRange} 130 | onSubmit = {onCreateReservation} 131 | disabled={isLoading} 132 | disabledDates={disabledDates } 133 | /> 134 |
135 |
136 |
137 |
138 |
139 | ); 140 | }; 141 | 142 | export default ListingClient; 143 | -------------------------------------------------------------------------------- /app/listings/[listingId]/page.tsx: -------------------------------------------------------------------------------- 1 | import getCurrentUser from "@/app/actions/getCurrentUser"; 2 | import getListingById from "@/app/actions/getListingById"; 3 | import ClientOnly from "@/app/components/ClientOnly"; 4 | import EmptyState from "@/app/components/EmptyState"; 5 | import ListingClient from "./ListingClient"; 6 | import getReservations from "@/app/actions/getReservations"; 7 | 8 | interface IParams { 9 | listingId?:string; 10 | } 11 | 12 | const ListingPage = async({params}:{params:IParams}) => { 13 | 14 | const listing = await getListingById(params) 15 | const reservations = await getReservations(params) 16 | const currentUser = await getCurrentUser() 17 | 18 | if(!listing){ 19 | return ( 20 | 21 | 22 | 23 | ) 24 | } 25 | 26 | return( 27 | 28 | 33 | 34 | ) 35 | } 36 | 37 | export default ListingPage; -------------------------------------------------------------------------------- /app/loading.tsx: -------------------------------------------------------------------------------- 1 | import Loader from "./components/Loader" 2 | 3 | const Loading = () =>{ 4 | return( 5 | 6 | ) 7 | } 8 | 9 | export default Loading; -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import Container from "@/app/components/Container"; 4 | import ListingCard from "@/app/components/listings/ListingCard"; 5 | import EmptyState from "@/app/components/EmptyState"; 6 | 7 | import getListings, { IListingsParams } from "@/app/actions/getListings"; 8 | import getCurrentUser from "@/app/actions/getCurrentUser"; 9 | import ClientOnly from "./components/ClientOnly"; 10 | 11 | interface HomeProps { 12 | searchParams: IListingsParams; 13 | } 14 | 15 | const Home = async ({ searchParams }: HomeProps) => { 16 | const currentUser = await getCurrentUser(); 17 | const listings = await getListings(searchParams); 18 | 19 | 20 | if (listings.length === 0) { 21 | return ( 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | return ( 29 | 30 | 31 |
43 | {listings.map((listing: any) => ( 44 | 49 | ))} 50 |
51 |
52 |
53 | ); 54 | }; 55 | 56 | export default Home; 57 | -------------------------------------------------------------------------------- /app/properties/PropertiesClient.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import React, { useCallback, useState } from 'react' 3 | import { SafeListing, SafeUser } from '../types' 4 | import Container from '../components/Container'; 5 | import Heading from '../components/Heading'; 6 | import { useRouter } from 'next/navigation'; 7 | import axios from 'axios'; 8 | import toast from 'react-hot-toast'; 9 | import ListingCard from '../components/listings/ListingCard'; 10 | 11 | 12 | interface PropertiesClientProps { 13 | listings: SafeListing []; 14 | currentUser?:SafeUser | null 15 | } 16 | const PropertiesClient:React.FC = ({ 17 | listings, 18 | currentUser 19 | }) => { 20 | const router = useRouter(); 21 | const [deletingId,setDeletingId] = useState('') 22 | 23 | const onCancel = useCallback((id:string) =>{ 24 | 25 | setDeletingId(id) 26 | axios.delete(`/api/listings/${id}`) 27 | .then(() => { 28 | toast.success('Listing Deleted') 29 | router.refresh() 30 | }) 31 | .catch((error) => { 32 | // toast.error(error?.response?.data?.error) 33 | toast.error(error) 34 | }) 35 | .finally(() =>{ 36 | setDeletingId('') 37 | }) 38 | },[router]) 39 | return ( 40 | 41 | 45 |
55 | {listings.map((listing) => ( 56 | 65 | ))} 66 |
67 |
68 | ) 69 | 70 | } 71 | 72 | export default PropertiesClient -------------------------------------------------------------------------------- /app/properties/page.tsx: -------------------------------------------------------------------------------- 1 | import ClientOnly from '../components/ClientOnly' 2 | import EmptyState from '../components/EmptyState' 3 | 4 | import getCurrentUser from '../actions/getCurrentUser' 5 | import PropertiesClient from './PropertiesClient' 6 | import getListings from '../actions/getListings' 7 | 8 | 9 | const PropertiesPage = async() => { 10 | const currentUser = await getCurrentUser(); 11 | if(!currentUser){ 12 | return( 13 | 14 | 18 | 19 | ) 20 | } 21 | 22 | const listings = await getListings({ 23 | userId: currentUser.id, 24 | }) 25 | 26 | if(listings.length === 0){ 27 | return ( 28 | 29 | 33 | 34 | ) 35 | } 36 | 37 | return( 38 | 39 | 43 | 44 | ) 45 | 46 | } 47 | export default PropertiesPage; 48 | 49 | -------------------------------------------------------------------------------- /app/providers/ToasterProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import {Toaster} from "react-hot-toast" 3 | 4 | const ToasterProvider = () =>{ 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default ToasterProvider -------------------------------------------------------------------------------- /app/reservations/ReservationsClient.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | "use client" 3 | import axios from "axios"; 4 | import React, { useCallback, useState } from "react"; 5 | import toast from "react-hot-toast"; 6 | import { SafeReservation, SafeUser } from "../types"; 7 | import Heading from "../components/Heading"; 8 | import Container from "../components/Container"; 9 | import ListingCard from "../components/listings/ListingCard"; 10 | import { useRouter } from "next/navigation"; 11 | 12 | interface ReservationsClientProps { 13 | reservations: SafeReservation[]; 14 | currentUser?: SafeUser | null; 15 | } 16 | const ReservationsClient: React.FC = ({ 17 | reservations, 18 | currentUser, 19 | }) => { 20 | const router = useRouter(); 21 | const [deletingId, setDeletingId] = useState(""); 22 | 23 | const onCancel = useCallback( 24 | (id: string) => { 25 | setDeletingId(id); 26 | axios 27 | .delete(`/api/reservations/${id}`) 28 | .then(() => { 29 | toast.success("Reservation canceled"); 30 | router.refresh(); 31 | }) 32 | .catch(() => { 33 | toast.error("Something went wrong !"); 34 | }) 35 | .finally(() => { 36 | setDeletingId(""); 37 | }); 38 | }, 39 | [router] 40 | ); 41 | return ( 42 | 43 | 44 |
56 | {reservations.map((reservation) => ( 57 | 67 | ) 68 | )} 69 |
70 |
71 | ); 72 | }; 73 | 74 | 75 | export default ReservationsClient; 76 | -------------------------------------------------------------------------------- /app/reservations/page.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import getCurrentUser from "../actions/getCurrentUser"; 4 | import getReservations from "../actions/getReservations"; 5 | import ClientOnly from "../components/ClientOnly"; 6 | import EmptyState from "../components/EmptyState"; 7 | import ReservationsClient from "./ReservationsClient"; 8 | 9 | const ReservationsPage = async () => { 10 | const currentUser = await getCurrentUser(); 11 | 12 | if (!currentUser) { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | const reservations = await getReservations({ 21 | authorId: currentUser.id, 22 | }); 23 | // console.log(reservations); 24 | 25 | if (reservations.length === 0) { 26 | return ( 27 | 28 | 32 | 33 | ); 34 | } 35 | 36 | return ( 37 | 38 | 42 | 43 | ); 44 | }; 45 | 46 | export default ReservationsPage; 47 | -------------------------------------------------------------------------------- /app/trips/TripsClient.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import React, { useCallback, useState } from 'react' 3 | import { SafeReservation, SafeUser } from '../types' 4 | import Container from '../components/Container'; 5 | import Heading from '../components/Heading'; 6 | import { useRouter } from 'next/navigation'; 7 | import axios from 'axios'; 8 | import toast from 'react-hot-toast'; 9 | import ListingCard from '../components/listings/ListingCard'; 10 | 11 | 12 | interface TripsClientProps { 13 | reservations: SafeReservation[]; 14 | currentUser?:SafeUser | null 15 | } 16 | const TripsClient:React.FC = ({ 17 | reservations, 18 | currentUser 19 | }) => { 20 | const router = useRouter(); 21 | const [deletingId,setDeletingId] = useState('') 22 | 23 | const onCancel = useCallback((id:string) =>{ 24 | 25 | setDeletingId(id) 26 | axios.delete(`/api/reservations/${id}`) 27 | .then(() => { 28 | toast.success('Reservation cancelled') 29 | router.refresh() 30 | }) 31 | .catch((error) => { 32 | // toast.error(error?.response?.data?.error) 33 | toast.error(error) 34 | }) 35 | .finally(() =>{ 36 | setDeletingId('') 37 | }) 38 | },[router]) 39 | return ( 40 | 41 | 45 |
55 | {reservations.map((reservation) => ( 56 | 66 | ))} 67 |
68 |
69 | ) 70 | 71 | } 72 | 73 | export default TripsClient -------------------------------------------------------------------------------- /app/trips/page.tsx: -------------------------------------------------------------------------------- 1 | import ClientOnly from '../components/ClientOnly' 2 | import EmptyState from '../components/EmptyState' 3 | 4 | import getCurrentUser from '../actions/getCurrentUser' 5 | import getReservations from '../actions/getReservations' 6 | import TripsClient from './TripsClient' 7 | 8 | 9 | const TripsPage = async() => { 10 | const currentUser = await getCurrentUser(); 11 | if(!currentUser){ 12 | return( 13 | 14 | 18 | 19 | ) 20 | } 21 | 22 | const reservations = await getReservations({ 23 | userId: currentUser.id, 24 | }) 25 | 26 | if(reservations.length === 0){ 27 | return ( 28 | 29 | 33 | 34 | ) 35 | } 36 | 37 | return( 38 | 39 | 43 | 44 | ) 45 | 46 | } 47 | export default TripsPage; 48 | 49 | -------------------------------------------------------------------------------- /app/types/index.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { User } from "@prisma/client"; 4 | import { Listing, Reservation } from "@prisma/client"; 5 | 6 | export type SafeListing = Omit & { 7 | createdAt: string; 8 | }; 9 | 10 | export type SafeReservation = Omit< 11 | Reservation, 12 | "createdAt" | "startDate" | "endDate" | "listing" 13 | > & { 14 | createdAt: string; 15 | startDate: string; 16 | endDate: string; 17 | listing: SafeListing; 18 | }; 19 | 20 | export type SafeUser = Omit< 21 | User, 22 | "createdAt" | "updatedAt" | "emailVerified" 23 | > & { 24 | createdAt?: string; 25 | updatedAt?: string; 26 | emailVerified?: string | null; 27 | }; 28 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | export {default} from 'next-auth/middleware'; 2 | export const config = { 3 | matcher:[ 4 | "/trips", 5 | "/reservations", 6 | "/properties", 7 | "/favorites", 8 | ] 9 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | // /** @type {import('next').NextConfig} */ 2 | // const nextConfig = { 3 | // experimental:{ 4 | // appDir:true, 5 | // }, 6 | // images:{ 7 | // domains:[ 8 | // "avatars.githubusercontent.com" 9 | // ] 10 | // } 11 | // } 12 | 13 | // module.exports = nextConfig 14 | 15 | /** @type {import('next').NextConfig} */ 16 | const nextConfig = { 17 | images: { 18 | domains: [ 19 | "avatars.githubusercontent.com", 20 | "lh3.googleusercontent.com", 21 | "res.cloudinary.com" 22 | ] 23 | } 24 | }; 25 | 26 | module.exports = nextConfig; 27 | 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airbnb-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "prisma generate && next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@next-auth/prisma-adapter": "^1.0.7", 13 | "@types/bcrypt": "^5.0.2", 14 | "axios": "^1.6.3", 15 | "bcrypt": "^5.1.1", 16 | "date-fns": "^3.3.0", 17 | "leaflet": "^1.9.4", 18 | "next": "14.0.4", 19 | "next-cloudinary": "^5.20.0", 20 | "query-string": "^8.1.0", 21 | "react": "^18", 22 | "react-date-range": "^2.0.0-alpha.4", 23 | "react-dom": "^18", 24 | "react-hook-form": "^7.49.2", 25 | "react-hot-toast": "^2.4.1", 26 | "react-icons": "^4.12.0", 27 | "react-leaflet": "^4.2.1", 28 | "react-select": "^5.8.0", 29 | "react-spinners": "^0.13.8", 30 | "world-countries": "^5.0.0", 31 | "zustand": "^4.4.7" 32 | }, 33 | "devDependencies": { 34 | "@types/leaflet": "^1.9.8", 35 | "@types/node": "^20", 36 | "@types/react": "^18", 37 | "@types/react-date-range": "^1.4.9", 38 | "@types/react-dom": "^18", 39 | "autoprefixer": "^10.0.1", 40 | "eslint": "^8", 41 | "eslint-config-next": "14.0.4", 42 | "postcss": "^8", 43 | "prisma": "^5.7.1", 44 | "tailwindcss": "^3.3.0", 45 | "typescript": "^5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcrypt" 2 | import NextAuth, { AuthOptions } from "next-auth" 3 | import CredentialsProvider from "next-auth/providers/credentials" 4 | import GithubProvider from "next-auth/providers/github" 5 | import GoogleProvider from "next-auth/providers/google" 6 | import { PrismaAdapter } from "@next-auth/prisma-adapter" 7 | 8 | import prisma from "@/app/libs/prismadb" 9 | 10 | export const authOptions: AuthOptions = { 11 | adapter: PrismaAdapter(prisma), 12 | providers: [ 13 | GithubProvider({ 14 | clientId: process.env.GITHUB_ID as string, 15 | clientSecret: process.env.GITHUB_SECRET as string 16 | }), 17 | GoogleProvider({ 18 | clientId: process.env.GOOGLE_CLIENT_ID as string, 19 | clientSecret: process.env.GOOGLE_CLIENT_SECRET as string 20 | }), 21 | CredentialsProvider({ 22 | name: 'credentials', 23 | credentials: { 24 | email: { label: 'email', type: 'text' }, 25 | password: { label: 'password', type: 'password' } 26 | }, 27 | async authorize(credentials) { 28 | // console.log(credentials); 29 | 30 | if (!credentials?.email || !credentials?.password) { 31 | throw new Error('Invalid credentials'); 32 | } 33 | 34 | const user = await prisma.user.findUnique({ 35 | where: { 36 | email: credentials.email 37 | } 38 | }); 39 | 40 | if (!user || !user?.hashedPassword) { 41 | throw new Error('Invalid credentials'); 42 | } 43 | 44 | const isCorrectPassword = await bcrypt.compare( 45 | credentials.password, 46 | user.hashedPassword 47 | ); 48 | 49 | if (!isCorrectPassword) { 50 | throw new Error('Invalid credentials'); 51 | } 52 | 53 | return user; 54 | } 55 | }) 56 | ], 57 | pages: { 58 | signIn: '/', 59 | }, 60 | debug: process.env.NODE_ENV === 'development', 61 | session: { 62 | strategy: "jwt", 63 | }, 64 | secret: process.env.NEXTAUTH_SECRET, 65 | } 66 | 67 | export default NextAuth(authOptions); 68 | 69 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "mongodb" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model User { 14 | id String @id @default(auto()) @map("_id") @db.ObjectId 15 | name String? 16 | email String? @unique 17 | emailVerified DateTime? 18 | image String? 19 | hashedPassword String? 20 | createdAt DateTime? @default(now()) 21 | updatedAt DateTime? @updatedAt 22 | favoriteIds String[] @db.ObjectId 23 | 24 | accounts Account[] 25 | listings Listing[] 26 | reservations Reservation[] 27 | } 28 | 29 | model Account { 30 | id String @id @default(auto()) @map("_id") @db.ObjectId 31 | userId String @db.ObjectId 32 | type String 33 | provider String 34 | providerAccountId String 35 | refresh_token String? @db.String 36 | access_token String? @db.String 37 | expires_at Int? 38 | token_type String? 39 | scope String? 40 | id_token String? 41 | session_state String? 42 | 43 | user User @relation(fields:[userId],references: [id] ,onDelete:Cascade) 44 | 45 | @@unique([provider,providerAccountId]) 46 | 47 | } 48 | model Listing { 49 | id String @id @default(auto()) @map("_id") @db.ObjectId 50 | title String 51 | description String 52 | imageSrc String 53 | createdAt DateTime @default(now()) 54 | category String 55 | roomCount Int 56 | bathroomCount Int 57 | guestCount Int 58 | locationValue String 59 | userId String @db.ObjectId 60 | price Int 61 | 62 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 63 | reservations Reservation[] 64 | } 65 | 66 | model Reservation { 67 | id String @id @default(auto()) @map("_id") @db.ObjectId 68 | userId String @db.ObjectId 69 | listingId String @db.ObjectId 70 | startDate DateTime 71 | endDate DateTime 72 | totalPrice Int 73 | createdAt DateTime @default(now()) 74 | 75 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 76 | listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade) 77 | } -------------------------------------------------------------------------------- /public/images/logo-ftw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiraadev/rent-application-nextjs/807d6dd342b2a9fcd3e8bc666c8fa1b971b15b9c/public/images/logo-ftw.png -------------------------------------------------------------------------------- /public/images/logo-yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiraadev/rent-application-nextjs/807d6dd342b2a9fcd3e8bc666c8fa1b971b15b9c/public/images/logo-yellow.png -------------------------------------------------------------------------------- /public/images/profile-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiraadev/rent-application-nextjs/807d6dd342b2a9fcd3e8bc666c8fa1b971b15b9c/public/images/profile-10.jpg -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/rent-app-screen-shot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiraadev/rent-application-nextjs/807d6dd342b2a9fcd3e8bc666c8fa1b971b15b9c/public/rent-app-screen-shot1.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------