├── .eslintrc.json ├── app ├── favicon.ico ├── libs │ ├── fetcher.ts │ └── prismadb.ts ├── loading.tsx ├── providers │ ├── ToasterProvider.tsx │ └── ModalsProvider.tsx ├── hooks │ ├── useRentModal.ts │ ├── useLoginModal.ts │ ├── useSearchModal.ts │ ├── useRegisterModal.ts │ ├── useCountries.ts │ └── useFavorite.ts ├── components │ ├── Loader.tsx │ ├── Avatar.tsx │ ├── navbar │ │ ├── Logo.tsx │ │ ├── MenuItem.tsx │ │ ├── Navbar.tsx │ │ ├── Categories.tsx │ │ ├── Search.tsx │ │ └── UserMenu.tsx │ ├── Container.tsx │ ├── ClientOnly.tsx │ ├── Heading.tsx │ ├── inputs │ │ ├── Calendar.tsx │ │ ├── CategoryInput.tsx │ │ ├── CountrySelect.tsx │ │ ├── ImageUpload.tsx │ │ ├── Input.tsx │ │ └── Counter.tsx │ ├── listings │ │ ├── ListingCategory.tsx │ │ ├── ListingHead.tsx │ │ ├── ListingReservation.tsx │ │ ├── ListingInfo.tsx │ │ └── ListingCard.tsx │ ├── EmptyState.tsx │ ├── HeartButton.tsx │ ├── Map.tsx │ ├── Button.tsx │ ├── CategoryBox.tsx │ ├── modals │ │ ├── LoginModal.tsx │ │ ├── RegisterModal.tsx │ │ ├── SearchModal.tsx │ │ ├── Modal.tsx │ │ └── RentModal.tsx │ └── footer │ │ └── footer.tsx ├── error.tsx ├── globals.css ├── api │ ├── register │ │ └── route.ts │ ├── listings │ │ ├── [listingId] │ │ │ └── route.ts │ │ └── route.ts │ ├── reservations │ │ ├── [reservationId] │ │ │ └── route.ts │ │ └── route.ts │ └── favorites │ │ └── [listingId] │ │ └── route.ts ├── types │ └── index.ts ├── actions │ ├── getFavoriteListings.ts │ ├── getListingById.ts │ ├── getCurrentUser.ts │ ├── getReservations.ts │ └── getListings.ts ├── favorites │ ├── page.tsx │ └── FavoritesClient.tsx ├── listings │ └── [listingId] │ │ ├── page.tsx │ │ └── ListingClient.tsx ├── properties │ ├── page.tsx │ └── PropertiesClient.tsx ├── bookings │ ├── page.tsx │ └── BookingsClient.tsx ├── reservations │ ├── page.tsx │ └── ReservationsClient.tsx ├── page.tsx └── layout.tsx ├── public ├── images │ ├── logo.png │ └── placeholder.jpg ├── vercel.svg ├── thirteen.svg └── next.svg ├── readme-images ├── logo.png └── screenshot3.png ├── postcss.config.js ├── .vscode └── settings.json ├── middleware.ts ├── .env.example ├── tailwind.config.js ├── next.config.js ├── .gitignore ├── tsconfig.json ├── package.json ├── pages └── api │ └── auth │ └── [...nextauth].ts ├── prisma └── schema.prisma └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shreyashkadam/rentpal/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shreyashkadam/rentpal/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /readme-images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shreyashkadam/rentpal/HEAD/readme-images/logo.png -------------------------------------------------------------------------------- /public/images/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shreyashkadam/rentpal/HEAD/public/images/placeholder.jpg -------------------------------------------------------------------------------- /readme-images/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shreyashkadam/rentpal/HEAD/readme-images/screenshot3.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /app/libs/fetcher.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const fetcher = (url: string) => axios.get(url).then((res) => res.data); 4 | 5 | export default fetcher; 6 | -------------------------------------------------------------------------------- /app/loading.tsx: -------------------------------------------------------------------------------- 1 | import Loader from "@/app/components/Loader"; 2 | 3 | const Loading = () => { 4 | return ( 5 | 6 | ); 7 | } 8 | 9 | export default Loading; -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | export { default } from "next-auth/middleware"; 2 | 3 | export const config = { 4 | matcher: ["/bookings", "/reservations", "/properties", "/favorites"], 5 | }; 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL= 2 | NEXTAUTH_SECRET= 3 | 4 | GITHUB_ID= 5 | GITHUB_SECRET= 6 | 7 | GOOGLE_CLIENT_ID= 8 | GOOGLE_CLIENT_SECRET= 9 | 10 | NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME= 11 | -------------------------------------------------------------------------------- /app/providers/ToasterProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Toaster } from "react-hot-toast"; 4 | 5 | const ToasterProvider = () => { 6 | return ( 7 | 8 | ); 9 | } 10 | 11 | export default ToasterProvider; 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./app/**/*.{js,ts,jsx,tsx}", 5 | "./pages/**/*.{js,ts,jsx,tsx}", 6 | "./components/**/*.{js,ts,jsx,tsx}", 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [], 12 | } 13 | -------------------------------------------------------------------------------- /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") globalThis.prisma = client 9 | 10 | export default client 11 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true, 5 | }, 6 | images: { 7 | domains: [ 8 | 'res.cloudinary.com', 9 | 'avatars.githubusercontent.com', 10 | 'lh3.googleusercontent.com' 11 | ] 12 | } 13 | } 14 | 15 | module.exports = nextConfig 16 | -------------------------------------------------------------------------------- /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 | 16 | export default useRentModal; 17 | -------------------------------------------------------------------------------- /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 | 16 | export default useLoginModal; 17 | -------------------------------------------------------------------------------- /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 | 16 | export default useSearchModal; 17 | -------------------------------------------------------------------------------- /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 | 16 | export default useRegisterModal; 17 | -------------------------------------------------------------------------------- /app/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PuffLoader } from "react-spinners"; 4 | 5 | const Loader = () => { 6 | return ( 7 |
16 | 20 |
21 | ); 22 | } 23 | 24 | export default Loader; -------------------------------------------------------------------------------- /app/components/Avatar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Image from "next/image"; 4 | 5 | interface AvatarProps { 6 | src: string | null | undefined; 7 | } 8 | 9 | const Avatar: React.FC = ({ src }) => { 10 | return ( 11 | Avatar 18 | ); 19 | } 20 | 21 | export default Avatar; -------------------------------------------------------------------------------- /app/components/navbar/Logo.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Image from "next/image"; 4 | import { useRouter } from "next/navigation"; 5 | 6 | const Logo = () => { 7 | const router = useRouter(); 8 | 9 | return ( 10 | router.push('/')} 12 | className="hidden md:block cursor-pointer" 13 | src="/images/logo.png" 14 | height="100" 15 | width="150" 16 | alt="Logo" 17 | /> 18 | ); 19 | } 20 | 21 | export default Logo; 22 | -------------------------------------------------------------------------------- /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 |
20 | {children} 21 |
22 | ); 23 | } 24 | 25 | export default Container; 26 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect } from "react"; 4 | 5 | import EmptyState from "@/app/components/EmptyState"; 6 | 7 | interface ErrorStateProps { 8 | error: Error 9 | } 10 | 11 | const ErrorState: React.FC = ({ error }) => { 12 | useEffect(() => { 13 | console.error(error); 14 | }, [error]); 15 | 16 | return ( 17 | 21 | ); 22 | } 23 | 24 | export default ErrorState; 25 | -------------------------------------------------------------------------------- /app/components/navbar/MenuItem.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | interface MenuItemProps { 4 | onClick: () => void; 5 | label: string; 6 | } 7 | 8 | const MenuItem: React.FC = ({ 9 | onClick, 10 | label 11 | }) => { 12 | return ( 13 |
23 | {label} 24 |
25 | ); 26 | } 27 | 28 | export default MenuItem; -------------------------------------------------------------------------------- /app/providers/ModalsProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import LoginModal from "../components/modals/LoginModal"; 4 | import RegisterModal from "../components/modals/RegisterModal"; 5 | import RentModal from "../components/modals/RentModal"; 6 | import SearchModal from "../components/modals/SearchModal"; 7 | 8 | const ModalsProvider = () => { 9 | return ( 10 | <> 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default ModalsProvider; -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.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 | -------------------------------------------------------------------------------- /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/Heading.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | interface HeadingProps { 4 | title: string; 5 | subtitle?: string; 6 | center?: boolean; 7 | } 8 | 9 | const Heading: React.FC = ({ 10 | title, 11 | subtitle, 12 | center 13 | }) => { 14 | return ( 15 |
16 |
17 | {title} 18 |
19 |
20 | {subtitle} 21 |
22 |
23 | ); 24 | } 25 | 26 | export default Heading; -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | :root { 8 | height: 100%; 9 | } 10 | 11 | html { 12 | background: #000; 13 | filter: invert(1) hue-rotate(180deg) 14 | } 15 | 16 | html img { 17 | filter: invert(1) hue-rotate(180deg) 18 | } 19 | 20 | .leaflet-bottom, 21 | .leaflet-control, 22 | .leaflet-pane, 23 | .leaflet-top { 24 | z-index: 0 !important; 25 | } 26 | 27 | .rdrMonth { 28 | width: 100% !important; 29 | } 30 | 31 | .rdrCalendarWrapper { 32 | font-size: 16px !important; 33 | width: 100% !important; 34 | } 35 | -------------------------------------------------------------------------------- /app/api/register/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import bcrypt from "bcrypt"; 3 | 4 | import prisma from "@/app/libs/prismadb"; 5 | 6 | export async function POST( 7 | request: Request, 8 | ) { 9 | const body = await request.json(); 10 | const { 11 | email, 12 | name, 13 | password, 14 | } = body; 15 | 16 | const hashedPassword = await bcrypt.hash(password, 12); 17 | 18 | const user = await prisma.user.create({ 19 | data: { 20 | email, 21 | name, 22 | hashedPassword, 23 | } 24 | }); 25 | 26 | return NextResponse.json(user); 27 | } 28 | -------------------------------------------------------------------------------- /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; 25 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/types/index.ts: -------------------------------------------------------------------------------- 1 | import { Listing, Reservation, User } from "@prisma/client"; 2 | 3 | export type SafeListing = Omit & { 4 | createdAt: string; 5 | }; 6 | 7 | export type SafeReservation = Omit< 8 | Reservation, 9 | "createdAt" | "startDate" | "endDate" | "listing" | "user" 10 | > & { 11 | createdAt: string; 12 | startDate: string; 13 | endDate: string; 14 | listing: SafeListing; 15 | user: SafeUser; 16 | }; 17 | 18 | export type SafeUser = Omit< 19 | User, 20 | "createdAt" | "updatedAt" | "emailVerified" 21 | > & { 22 | createdAt: string; 23 | updatedAt: string; 24 | emailVerified: string | null; 25 | }; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /app/actions/getFavoriteListings.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/app/libs/prismadb"; 2 | 3 | import getCurrentUser from "./getCurrentUser"; 4 | 5 | export default async function getFavoriteListings() { 6 | try { 7 | const currentUser = await getCurrentUser(); 8 | 9 | if (!currentUser) { 10 | return []; 11 | } 12 | 13 | const favorites = await prisma.listing.findMany({ 14 | where: { 15 | id: { 16 | in: [...(currentUser.favoriteIds || [])] 17 | } 18 | } 19 | }); 20 | 21 | const safeFavorites = favorites.map((favorite) => ({ 22 | ...favorite, 23 | createdAt: favorite.createdAt.toString(), 24 | })); 25 | 26 | return safeFavorites; 27 | } catch (error: any) { 28 | throw new Error(error); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/api/listings/[listingId]/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 | listingId?: string; 8 | } 9 | 10 | export async function DELETE( 11 | request: Request, 12 | { params }: { params: IParams } 13 | ) { 14 | const currentUser = await getCurrentUser(); 15 | 16 | if (!currentUser) { 17 | return NextResponse.error(); 18 | } 19 | 20 | const { listingId } = params; 21 | 22 | if (!listingId || typeof listingId !== 'string') { 23 | throw new Error('Invalid ID'); 24 | } 25 | 26 | const listing = await prisma.listing.deleteMany({ 27 | where: { 28 | id: listingId, 29 | userId: currentUser.id 30 | } 31 | }); 32 | 33 | return NextResponse.json(listing); 34 | } 35 | -------------------------------------------------------------------------------- /app/components/inputs/Calendar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | DateRange, 5 | Range, 6 | RangeKeyDict 7 | } from 'react-date-range'; 8 | 9 | import 'react-date-range/dist/styles.css'; 10 | import 'react-date-range/dist/theme/default.css'; 11 | 12 | interface DatePickerProps { 13 | value: Range, 14 | onChange: (value: RangeKeyDict) => void; 15 | disabledDates?: Date[]; 16 | } 17 | 18 | const DatePicker: React.FC = ({ 19 | value, 20 | onChange, 21 | disabledDates 22 | }) => { 23 | return ( 24 | 34 | ); 35 | } 36 | 37 | export default DatePicker; 38 | -------------------------------------------------------------------------------- /app/components/inputs/CategoryInput.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { IconType } from "react-icons"; 4 | 5 | interface CategoryBoxProps { 6 | icon: IconType, 7 | label: string; 8 | selected?: boolean; 9 | onClick: (value: string) => void; 10 | } 11 | 12 | const CategoryBox: React.FC = ({ 13 | icon: Icon, 14 | label, 15 | selected, 16 | onClick 17 | }) => { 18 | return ( 19 |
onClick(label)} 21 | className={` 22 | rounded-xl 23 | border-2 24 | p-4 25 | flex 26 | flex-col 27 | gap-3 28 | hover:border-black 29 | transition 30 | cursor-pointer 31 | ${selected ? 'border-black' : 'border-neutral-200'} 32 | `} 33 | > 34 | 35 |
36 | {label} 37 |
38 |
39 | ); 40 | } 41 | 42 | export default CategoryBox; -------------------------------------------------------------------------------- /app/components/listings/ListingCategory.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { IconType } from "react-icons"; 4 | 5 | interface CategoryViewProps { 6 | icon: IconType, 7 | label: string, 8 | description: string 9 | } 10 | 11 | const CategoryView: React.FC = ({ 12 | icon: Icon, 13 | label, 14 | description 15 | }) => { 16 | return ( 17 |
18 |
19 | 20 |
21 |
24 | {label} 25 |
26 |
29 | {description} 30 |
31 |
32 |
33 |
34 | ); 35 | } 36 | 37 | export default CategoryView; -------------------------------------------------------------------------------- /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.toString(), 29 | user: { 30 | ...listing.user, 31 | createdAt: listing.user.createdAt.toString(), 32 | updatedAt: listing.user.updatedAt.toString(), 33 | emailVerified: 34 | listing.user.emailVerified?.toString() || null, 35 | } 36 | }; 37 | } catch (error: any) { 38 | throw new Error(error); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/favorites/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | import EmptyState from "@/app/components/EmptyState"; 3 | import ClientOnly from "@/app/components/ClientOnly"; 4 | 5 | import getCurrentUser from "@/app/actions/getCurrentUser"; 6 | import getFavoriteListings from "@/app/actions/getFavoriteListings"; 7 | 8 | import FavoritesClient from "./FavoritesClient"; 9 | 10 | const ListingPage = async () => { 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; 36 | -------------------------------------------------------------------------------- /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 | ) { 14 | const currentUser = await getCurrentUser(); 15 | 16 | if (!currentUser) { 17 | return NextResponse.error(); 18 | } 19 | 20 | const { reservationId } = params; 21 | 22 | if (!reservationId || typeof reservationId !== 'string') { 23 | throw new Error('Invalid ID'); 24 | } 25 | 26 | const reservation = await prisma.reservation.deleteMany({ 27 | where: { 28 | id: reservationId, 29 | OR: [ 30 | { userId: currentUser.id }, 31 | { listing: { userId: currentUser.id } } 32 | ] 33 | } 34 | }); 35 | 36 | return NextResponse.json(reservation); 37 | } 38 | -------------------------------------------------------------------------------- /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/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 | itemCount, 20 | location, 21 | price, 22 | } = body; 23 | 24 | Object.keys(body).forEach((value: any) => { 25 | if (!body[value]) { 26 | NextResponse.error(); 27 | } 28 | }); 29 | 30 | const listing = await prisma.listing.create({ 31 | data: { 32 | title, 33 | description, 34 | imageSrc, 35 | category, 36 | itemCount, 37 | locationValue: location.value, 38 | price: parseInt(price, 10), 39 | userId: currentUser.id, 40 | }, 41 | }); 42 | 43 | return NextResponse.json(listing); 44 | } 45 | -------------------------------------------------------------------------------- /app/api/reservations/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( 7 | request: Request, 8 | ) { 9 | const currentUser = await getCurrentUser(); 10 | 11 | if (!currentUser) { 12 | return NextResponse.error(); 13 | } 14 | 15 | const body = await request.json(); 16 | const { 17 | listingId, 18 | startDate, 19 | endDate, 20 | totalPrice 21 | } = body; 22 | 23 | if (!listingId || !startDate || !endDate || !totalPrice) { 24 | return NextResponse.error(); 25 | } 26 | 27 | const listingAndReservation = await prisma.listing.update({ 28 | where: { 29 | id: listingId 30 | }, 31 | data: { 32 | reservations: { 33 | create: { 34 | userId: currentUser.id, 35 | startDate, 36 | endDate, 37 | totalPrice, 38 | } 39 | } 40 | } 41 | }); 42 | 43 | return NextResponse.json(listingAndReservation); 44 | } 45 | -------------------------------------------------------------------------------- /app/listings/[listingId]/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | import getCurrentUser from "@/app/actions/getCurrentUser"; 3 | import getListingById from "@/app/actions/getListingById"; 4 | import getReservations from "@/app/actions/getReservations"; 5 | 6 | import ClientOnly from "@/app/components/ClientOnly"; 7 | import EmptyState from "@/app/components/EmptyState"; 8 | 9 | import ListingClient from "./ListingClient"; 10 | 11 | interface IParams { 12 | listingId?: string; 13 | } 14 | 15 | const ListingPage = async ({ params }: { params: IParams }) => { 16 | 17 | const listing = await getListingById(params); 18 | const reservations = await getReservations(params); 19 | const currentUser = await getCurrentUser(); 20 | 21 | if (!listing) { 22 | return ( 23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | return ( 30 | 31 | 36 | 37 | ); 38 | } 39 | 40 | export default ListingPage; 41 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/properties/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | import EmptyState from "@/app/components/EmptyState"; 3 | import ClientOnly from "@/app/components/ClientOnly"; 4 | 5 | import getCurrentUser from "@/app/actions/getCurrentUser"; 6 | import getListings from "@/app/actions/getListings"; 7 | 8 | import PropertiesClient from "./PropertiesClient"; 9 | 10 | const PropertiesPage = async () => { 11 | const currentUser = await getCurrentUser(); 12 | 13 | if (!currentUser) { 14 | return 18 | } 19 | 20 | const listings = await getListings({ userId: currentUser.id }); 21 | 22 | if (listings.length === 0) { 23 | return ( 24 | 25 | 29 | 30 | ); 31 | } 32 | 33 | return ( 34 | 35 | 39 | 40 | ); 41 | } 42 | 43 | export default PropertiesPage; 44 | -------------------------------------------------------------------------------- /app/components/navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { SafeUser } from "@/app/types"; 2 | 3 | import Categories from "./Categories"; 4 | import Container from "../Container"; 5 | import Logo from "./Logo"; 6 | import Search from "./Search"; 7 | import UserMenu from "./UserMenu"; 8 | 9 | interface NavbarProps { 10 | currentUser?: SafeUser | null; 11 | } 12 | 13 | const Navbar: React.FC = ({ 14 | currentUser, 15 | }) => { 16 | return ( 17 |
18 |
25 | 26 |
36 | 37 | 38 | 39 |
40 |
41 |
42 | 43 |
44 | ); 45 | } 46 | 47 | 48 | export default Navbar; -------------------------------------------------------------------------------- /app/bookings/page.tsx: -------------------------------------------------------------------------------- 1 | import EmptyState from "@/app/components/EmptyState"; 2 | import ClientOnly from "@/app/components/ClientOnly"; 3 | 4 | import getCurrentUser from "@/app/actions/getCurrentUser"; 5 | import getReservations from "@/app/actions/getReservations"; 6 | 7 | import BookingsClient from "./BookingsClient"; 8 | 9 | const BookingsPage = async () => { 10 | const currentUser = await getCurrentUser(); 11 | 12 | if (!currentUser) { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | const reservations = await getReservations({ userId: currentUser.id }); 21 | 22 | if (reservations.length === 0) { 23 | return ( 24 | 25 | 29 | 30 | ); 31 | } 32 | 33 | return ( 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default BookingsPage; 41 | -------------------------------------------------------------------------------- /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/reservations/page.tsx: -------------------------------------------------------------------------------- 1 | import EmptyState from "@/app/components/EmptyState"; 2 | import ClientOnly from "@/app/components/ClientOnly"; 3 | 4 | import getCurrentUser from "@/app/actions/getCurrentUser"; 5 | import getReservations from "@/app/actions/getReservations"; 6 | 7 | import BookingsClient 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({ authorId: currentUser.id }); 21 | 22 | if (reservations.length === 0) { 23 | return ( 24 | 25 | 29 | 30 | ); 31 | } 32 | 33 | return ( 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default ReservationsPage; 41 | -------------------------------------------------------------------------------- /app/components/HeartButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { AiFillHeart, AiOutlineHeart } from "react-icons/ai"; 4 | 5 | import useFavorite from "@/app/hooks/useFavorite"; 6 | import { SafeUser } from "@/app/types"; 7 | 8 | import ClientOnly from "./ClientOnly"; 9 | 10 | interface HeartButtonProps { 11 | listingId: string 12 | currentUser?: SafeUser | null 13 | } 14 | 15 | const HeartButton: React.FC = ({ 16 | listingId, 17 | currentUser 18 | }) => { 19 | const { hasFavorited, toggleFavorite } = useFavorite({ 20 | listingId, 21 | currentUser 22 | }); 23 | 24 | return ( 25 |
34 | 43 | 49 |
50 | ); 51 | } 52 | 53 | // 54 | 55 | export default HeartButton; -------------------------------------------------------------------------------- /app/favorites/FavoritesClient.tsx: -------------------------------------------------------------------------------- 1 | import { SafeListing, SafeUser } from "@/app/types"; 2 | 3 | import Heading from "@/app/components/Heading"; 4 | import Container from "@/app/components/Container"; 5 | import ListingCard from "@/app/components/listings/ListingCard"; 6 | 7 | interface FavoritesClientProps { 8 | listings: SafeListing[], 9 | currentUser?: SafeUser | null, 10 | } 11 | 12 | const FavoritesClient: React.FC = ({ 13 | listings, 14 | currentUser 15 | }) => { 16 | return ( 17 | 18 | 22 |
35 | {listings.map((listing: any) => ( 36 | 41 | ))} 42 |
43 |
44 | ); 45 | } 46 | 47 | export default FavoritesClient; -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/Map.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import L from 'leaflet'; 4 | import { MapContainer, Marker, TileLayer } from 'react-leaflet' 5 | 6 | import 'leaflet/dist/leaflet.css' 7 | import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png'; 8 | import markerIcon from 'leaflet/dist/images/marker-icon.png'; 9 | import markerShadow from 'leaflet/dist/images/marker-shadow.png'; 10 | 11 | // @ts-ignore 12 | delete L.Icon.Default.prototype._getIconUrl; 13 | L.Icon.Default.mergeOptions({ 14 | iconUrl: markerIcon.src, 15 | iconRetinaUrl: markerIcon2x.src, 16 | shadowUrl: markerShadow.src, 17 | }); 18 | 19 | interface MapProps { 20 | center?: number[] 21 | } 22 | 23 | const url = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; 24 | const attribution = '© OpenStreetMap contributors'; 25 | 26 | const Map: React.FC = ({ center }) => { 27 | return ( 28 | 34 | 38 | {center && ( 39 | 40 | )} 41 | 42 | ) 43 | } 44 | 45 | export default Map -------------------------------------------------------------------------------- /app/components/Button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { IconType } from "react-icons"; 4 | 5 | interface ButtonProps { 6 | label: string; 7 | onClick: (e: React.MouseEvent) => void; 8 | disabled?: boolean; 9 | outline?: boolean; 10 | small?: boolean; 11 | icon?: IconType; 12 | } 13 | 14 | const Button: React.FC = ({ 15 | label, 16 | onClick, 17 | disabled, 18 | outline, 19 | small, 20 | icon: Icon, 21 | }) => { 22 | return ( 23 | 55 | ); 56 | } 57 | 58 | export default Button; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rentpal", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@next-auth/prisma-adapter": "^1.0.5", 13 | "@prisma/client": "^4.11.0", 14 | "@types/node": "18.15.5", 15 | "@types/react": "18.0.28", 16 | "@types/react-dom": "18.0.11", 17 | "axios": "^1.3.4", 18 | "bcrypt": "^5.1.0", 19 | "date-fns": "^2.29.3", 20 | "eslint": "8.36.0", 21 | "eslint-config-next": "13.2.4", 22 | "leaflet": "^1.9.3", 23 | "next": "13.2.4", 24 | "next-auth": "^4.20.1", 25 | "next-cloudinary": "^4.0.1", 26 | "next-superjson-plugin": "^0.5.6", 27 | "query-string": "^8.1.0", 28 | "react": "18.2.0", 29 | "react-date-range": "^1.4.0", 30 | "react-dom": "18.2.0", 31 | "react-hook-form": "^7.43.7", 32 | "react-hot-toast": "^2.4.0", 33 | "react-icons": "^4.8.0", 34 | "react-leaflet": "^4.2.1", 35 | "react-select": "^5.7.2", 36 | "react-spinners": "^0.13.8", 37 | "swr": "^2.1.1", 38 | "typescript": "5.0.2", 39 | "world-countries": "^4.0.0", 40 | "zustand": "^4.3.6" 41 | }, 42 | "devDependencies": { 43 | "@types/bcrypt": "^5.0.0", 44 | "@types/leaflet": "^1.9.3", 45 | "@types/react-date-range": "^1.4.4", 46 | "autoprefixer": "^10.4.14", 47 | "postcss": "^8.4.21", 48 | "prisma": "^4.11.0", 49 | "tailwindcss": "^3.2.7" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Container from "@/app/components/Container"; 2 | import ListingCard from "@/app/components/listings/ListingCard"; 3 | import EmptyState from "@/app/components/EmptyState"; 4 | 5 | import getListings, { 6 | IListingsParams 7 | } 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 listings = await getListings(searchParams); 17 | const currentUser = await getCurrentUser(); 18 | 19 | if (listings.length === 0) { 20 | return ( 21 | 22 | 23 | 24 | ); 25 | } 26 | 27 | return ( 28 | 29 | 30 |
43 | {listings.map((listing: any) => ( 44 | 49 | ))} 50 |
51 |
52 |
53 | ) 54 | } 55 | 56 | export default Home; 57 | -------------------------------------------------------------------------------- /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 | import Footer from '@/app/components/footer/footer'; 15 | 16 | export const metadata = { 17 | title: 'Rentpal', 18 | description: 'Rent your stuff', 19 | } 20 | 21 | const font = Nunito({ 22 | subsets: ['latin'], 23 | }); 24 | 25 | export default async function RootLayout({ 26 | children, 27 | }: { 28 | children: React.ReactNode 29 | }) { 30 | const currentUser = await getCurrentUser(); 31 | 32 | return ( 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | {children} 47 |
48 |