├── .env-example ├── .gitignore ├── README.md ├── _theme_files ├── add-property.html ├── css │ └── styles.css ├── favicon.ico ├── images │ ├── logo-white.png │ ├── logo.png │ ├── pin.svg │ ├── profile.png │ └── properties │ │ ├── a1.jpg │ │ ├── a2.jpg │ │ ├── a3.jpg │ │ ├── a4.jpg │ │ ├── b1.jpg │ │ ├── b2.jpg │ │ ├── b3.jpg │ │ ├── c1.jpg │ │ ├── c2.jpg │ │ ├── c3.jpg │ │ ├── c4.jpg │ │ ├── d1.jpg │ │ ├── d2.jpg │ │ ├── d3.jpg │ │ ├── e1.jpg │ │ ├── e2.jpg │ │ ├── e3.jpg │ │ ├── e4.jpg │ │ ├── f1.jpg │ │ ├── f2.jpg │ │ ├── f3.jpg │ │ ├── g1.jpg │ │ ├── g2.jpg │ │ ├── g3.jpg │ │ ├── g4.jpg │ │ ├── h1.jpg │ │ ├── h2.jpg │ │ ├── h3.jpg │ │ ├── i1.jpg │ │ ├── i2.jpg │ │ ├── i3.jpg │ │ ├── j1.jpg │ │ ├── j2.jpg │ │ └── j3.jpg ├── index.html ├── js │ └── main.js ├── messages.html ├── not-found.html ├── profile.html ├── properties.html ├── property.html └── saved-properties.html ├── app ├── actions │ ├── addMessage.js │ ├── addProperty.js │ ├── bookmarkProperty.js │ ├── checkBookmarkStatus.js │ ├── deleteMessage.js │ ├── deleteProperty.js │ ├── getUnreadMessageCount.js │ ├── markMessageAsRead.js │ └── updateProperty.js ├── api │ └── auth │ │ └── [...nextauth] │ │ └── route.js ├── error.jsx ├── favicon.ico ├── layout.jsx ├── loading.jsx ├── messages │ └── page.jsx ├── not-found.jsx ├── page.jsx ├── profile │ └── page.jsx └── properties │ ├── [id] │ ├── edit │ │ └── page.jsx │ └── page.jsx │ ├── add │ └── page.jsx │ ├── page.jsx │ ├── saved │ └── page.jsx │ └── search-results │ └── page.jsx ├── assets ├── images │ ├── logo-white.png │ ├── logo.png │ ├── pin.svg │ └── profile.png └── styles │ └── globals.css ├── components ├── AuthProvider.jsx ├── BookmarkButton.jsx ├── FeaturedProperties.jsx ├── FeaturedPropertyCard.jsx ├── Footer.jsx ├── Hero.jsx ├── HomeProperties.jsx ├── InfoBox.jsx ├── InfoBoxes.jsx ├── MessageCard.jsx ├── Navbar.jsx ├── Pagination.jsx ├── ProfileProperties.jsx ├── PropertyAddForm.jsx ├── PropertyCard.jsx ├── PropertyContactForm.jsx ├── PropertyDetails.jsx ├── PropertyEditForm.jsx ├── PropertyHeaderImage.jsx ├── PropertyImages.jsx ├── PropertyMap.jsx ├── PropertySearchForm.jsx ├── ShareButtons.jsx ├── Spinner.jsx ├── SubmitMessageButton.jsx └── UnreadMessageCount.jsx ├── config ├── cloudinary.js └── database.js ├── context └── GlobalContext.js ├── jsconfig.json ├── middleware.js ├── models ├── Message.js ├── Property.js └── User.js ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── properties.json ├── properties2.json ├── public ├── images │ └── screen.jpg ├── next.svg └── vercel.svg ├── tailwind.config.js └── utils ├── authOptions.js ├── convertToObject.js └── getSessionUser.js /.env-example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_DOMAIN=http://localhost:3000 2 | NEXT_PUBLIC_API_DOMAIN=http://localhost:3000/api 3 | MONGODB_URI=ADD_YOUR_OWN 4 | GOOGLE_CLIENT_ID=ADD_YOUR_OWN 5 | GOOGLE_CLIENT_SECRET=ADD_YOUR_OWN 6 | NEXTAUTH_URL=http://localhost:3000 7 | NEXTAUTH_URL_INTERNAL=http://localhost:3000 8 | NEXTAUTH_SECRET=ADD_YOUR_OWN 9 | CLOUDINARY_CLOUD_NAME=ADD_YOUR_OWN 10 | CLOUDINARY_API_KEY=ADD_YOUR_OWN 11 | CLOUDINARY_API_SECRET=ADD_YOUR_OWN 12 | NEXT_PUBLIC_MAPBOX_TOKEN=ADD_YOUR_OWN 13 | NEXT_PUBLIC_GOOGLE_GEOCODING_API_KEY=ADD_YOUR_OWN -------------------------------------------------------------------------------- /.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 | .DS_Store 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Property Pulse 2 | 3 | > A web application to help you find your next rental property. 4 | 5 | This is the main project from my [Next 14 From Scratch Course](https://www.traversymedia.com/nextjs-from-scratch) 6 | 7 | This is the second iteration of the app/course that uses server actions instead of API routes. 8 | 9 | The `_theme_files` folder contains the pure HTML files with Tailwind classes. 10 | 11 | 12 | 13 | ## Features 14 | 15 | Here are some of the current features that Property Pulse has: 16 | 17 | - [x] User authentication with Google & Next Auth 18 | - [x] User authorization 19 | - [x] Route protection 20 | - [x] User profile with user listings 21 | - [x] Property Listing CRUD 22 | - [x] Property image upload (Multiple) 23 | - [x] Property search 24 | - [x] Internal messages with 'unread' notifications 25 | - [x] Photoswipe image gallery 26 | - [x] Mapbox maps 27 | - [x] Toast notifications 28 | - [x] Property bookmarking / saved properties 29 | - [x] Property sharing to social media 30 | - [x] Loading spinners 31 | - [x] Responsive design (Tailwind) 32 | - [x] Custom 404 page 33 | - [x] Next.js Actions 34 | 35 | Property Pulse uses the following technologies: 36 | 37 | - [Next.js](https://nextjs.org/) 38 | - [React](https://reactjs.org/) 39 | - [Tailwind CSS](https://tailwindcss.com/) 40 | - [MongoDB](https://www.mongodb.com/) 41 | - [Mongoose](https://mongoosejs.com/) 42 | - [NextAuth.js](https://next-auth.js.org/) 43 | - [React Icons](https://react-icons.github.io/react-icons/) 44 | - [Photoswipe](https://photoswipe.com/) 45 | - [Cloudinary](https://cloudinary.com/) 46 | - [Mapbox](https://www.mapbox.com/) 47 | - [React Map GL](https://visgl.github.io/react-map-gl/) 48 | - [React Geocode](https://www.npmjs.com/package/react-geocode) 49 | - [React Spinners](https://www.npmjs.com/package/react-spinners) 50 | - [React Toastify](https://fkhadra.github.io/react-toastify/) 51 | - [React Share](https://www.npmjs.com/package/react-share) 52 | 53 | ## Getting Started 54 | 55 | ### Prerequisites 56 | 57 | - Node.js version 18 or higher 58 | - MongoDB Atlas account and a cluster. Sign up and create a cluster at [MongoDB](https://www.mongodb.com/) 59 | - Cloudinary account. Sign up at [Cloudinary](https://cloudinary.com/) 60 | - Google console account. Sign up at [Google Cloud](https://console.cloud.google.com/) 61 | - Mapbox account. Sign up at [Mapbox](https://www.mapbox.com/) 62 | 63 | ### `.env` File 64 | 65 | Rename the `env.example` file to `.env` and fill in the following environment variables: 66 | 67 | - Get your MongoDB connection string from your MongoDB Atlas cluster and add it to `MONGODB_URI`. 68 | - Get your Google client ID and secret from your Google console account and add them to `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`. 69 | - Add a secret to `NEXTAUTH_SECRET`. You can generate with the following command: 70 | ```bash 71 | openssl rand -base64 32 72 | ``` 73 | - Get your Cloudinary cloud name, API key, and API secret from your Cloudinary account and add them to `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_API_KEY`, and `CLOUDINARY_API_SECRET`. 74 | - Get your Mapbox token from your Mapbox account and add it to `NEXT_PUBLIC_MAPBOX_TOKEN`. 75 | - Get your Google Geocoding API key from your Google console account and add it to `NEXT_PUBLIC_GOOGLE_GEOCODING_API_KEY`. 76 | 77 | ### Install Dependencies 78 | 79 | ```bash 80 | npm install 81 | ``` 82 | 83 | ### Run the Development Server 84 | 85 | ```bash 86 | npm run dev 87 | ``` 88 | 89 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 90 | 91 | ## License 92 | 93 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 94 | -------------------------------------------------------------------------------- /_theme_files/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/favicon.ico -------------------------------------------------------------------------------- /_theme_files/images/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/logo-white.png -------------------------------------------------------------------------------- /_theme_files/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/logo.png -------------------------------------------------------------------------------- /_theme_files/images/pin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_theme_files/images/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/profile.png -------------------------------------------------------------------------------- /_theme_files/images/properties/a1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/a1.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/a2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/a2.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/a3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/a3.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/a4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/a4.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/b1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/b1.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/b2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/b2.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/b3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/b3.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/c1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/c1.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/c2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/c2.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/c3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/c3.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/c4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/c4.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/d1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/d1.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/d2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/d2.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/d3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/d3.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/e1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/e1.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/e2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/e2.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/e3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/e3.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/e4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/e4.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/f1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/f1.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/f2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/f2.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/f3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/f3.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/g1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/g1.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/g2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/g2.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/g3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/g3.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/g4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/g4.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/h1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/h1.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/h2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/h2.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/h3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/h3.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/i1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/i1.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/i2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/i2.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/i3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/i3.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/j1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/j1.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/j2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/j2.jpg -------------------------------------------------------------------------------- /_theme_files/images/properties/j3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/_theme_files/images/properties/j3.jpg -------------------------------------------------------------------------------- /_theme_files/js/main.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | // Mobile menu dropdown 3 | const mobileMenuButton = document.querySelector('#mobile-dropdown-button'); 4 | const mobileMenu = document.querySelector('#mobile-menu'); 5 | 6 | mobileMenuButton.addEventListener('click', () => 7 | mobileMenu.classList.toggle('hidden') 8 | ); 9 | 10 | // Profile dropdown 11 | const profileButton = document.querySelector('#user-menu-button'); 12 | const profileDropdown = document.querySelector('#user-menu'); 13 | 14 | profileButton.addEventListener('click', () => 15 | profileDropdown.classList.toggle('hidden') 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /_theme_files/messages.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | Property Pulse | Find local rental properties 16 | 17 | 18 | 217 | 218 |
219 |
220 |
223 |

Your Messages

224 | 225 |
226 |
229 |

230 | Property Inquiry: 231 | Boston Commons Retreat 232 |

233 |

234 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. 235 | Obcaecati libero nobis vero quos aspernatur nemo alias nam, odit 236 | dolores sed quaerat illum impedit quibusdam officia ad 237 | voluptatibus molestias sequi? Repudiandae! 238 |

239 | 240 |
    241 |
  • Name: John Doe
  • 242 | 243 |
  • 244 | Reply Email: 245 | recipient@example.com 248 |
  • 249 |
  • 250 | Reply Phone: 251 | 123-456-7890 254 |
  • 255 |
  • Received:1/1/2024 12:00 PM
  • 256 |
257 | 262 | 265 |
266 |
267 |
268 |
269 |
270 | 271 | 272 | 294 | 295 | 296 | 297 | 298 | -------------------------------------------------------------------------------- /_theme_files/not-found.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | Property Pulse | Find local rental properties 16 | 17 | 18 | 217 | 218 |
219 |
220 |
223 |
224 | 227 |
228 |
229 |

Page Not Found

230 |

231 | The page you are looking for does not exist. 232 |

233 | Go Home 238 |
239 |
240 |
241 |
242 |
243 | 244 | 245 | 267 | 268 | 269 | 270 | 271 | -------------------------------------------------------------------------------- /_theme_files/profile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | Property Pulse | Find local rental properties 16 | 17 | 18 | 217 | 218 |
219 |
220 |
223 |

Your Profile

224 |
225 |
226 |
227 | User 232 |
233 | 234 |

235 | Name: John Doe 236 |

237 |

238 | Email: john@gmail.com 239 |

240 |
241 | 242 |
243 |

Your Listings

244 |
245 | 246 | Property 1 251 | 252 |
253 |

Property Title 1

254 |

Address: 123 Main St

255 |
256 |
257 | 261 | Edit 262 | 263 | 269 |
270 |
271 |
272 | 273 | Property 2 278 | 279 |
280 |

Property Title 2

281 |

Address: 456 Elm St

282 |
283 |
284 | 288 | Edit 289 | 290 | 296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 | 304 | 305 | 327 | 328 | 329 | 330 | 331 | -------------------------------------------------------------------------------- /app/actions/addMessage.js: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | import connectDB from '@/config/database'; 3 | import Message from '@/models/Message'; 4 | import { getSessionUser } from '@/utils/getSessionUser'; 5 | import { revalidatePath } from 'next/cache'; 6 | 7 | async function addMessage(previousState, formData) { 8 | await connectDB(); 9 | 10 | const sessionUser = await getSessionUser(); 11 | 12 | if (!sessionUser || !sessionUser.user) { 13 | return { error: 'You must be logged in to send a message' }; 14 | } 15 | 16 | const { user } = sessionUser; 17 | 18 | const recipient = formData.get('recipient'); 19 | 20 | if (user.id === recipient) { 21 | return { error: 'You can not send a message to yourself' }; 22 | } 23 | 24 | const newMessage = new Message({ 25 | sender: user.id, 26 | recipient, 27 | property: formData.get('property'), 28 | name: formData.get('name'), 29 | email: formData.get('email'), 30 | phone: formData.get('phone'), 31 | body: formData.get('message'), 32 | }); 33 | 34 | await newMessage.save(); 35 | 36 | return { submitted: true }; 37 | } 38 | 39 | export default addMessage; 40 | -------------------------------------------------------------------------------- /app/actions/addProperty.js: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | import connectDB from '@/config/database'; 3 | import Property from '@/models/Property'; 4 | import { getSessionUser } from '@/utils/getSessionUser'; 5 | import { revalidatePath } from 'next/cache'; 6 | import { redirect } from 'next/navigation'; 7 | import cloudinary from '@/config/cloudinary'; 8 | 9 | async function addProperty(formData) { 10 | await connectDB(); 11 | 12 | const sessionUser = await getSessionUser(); 13 | 14 | if (!sessionUser || !sessionUser.userId) { 15 | throw new Error('User ID is required'); 16 | } 17 | 18 | const { userId } = sessionUser; 19 | 20 | // Access all values for amenities and images 21 | const amenities = formData.getAll('amenities'); 22 | const images = formData.getAll('images').filter((image) => image.name !== ''); 23 | 24 | // Create the propertyData object with embedded seller_info 25 | const propertyData = { 26 | type: formData.get('type'), 27 | name: formData.get('name'), 28 | description: formData.get('description'), 29 | location: { 30 | street: formData.get('location.street'), 31 | city: formData.get('location.city'), 32 | state: formData.get('location.state'), 33 | zipcode: formData.get('location.zipcode'), 34 | }, 35 | beds: formData.get('beds'), 36 | baths: formData.get('baths'), 37 | square_feet: formData.get('square_feet'), 38 | amenities, 39 | rates: { 40 | weekly: formData.get('rates.weekly'), 41 | monthly: formData.get('rates.monthly'), 42 | nightly: formData.get('rates.nightly.'), 43 | }, 44 | seller_info: { 45 | name: formData.get('seller_info.name'), 46 | email: formData.get('seller_info.email'), 47 | phone: formData.get('seller_info.phone'), 48 | }, 49 | owner: userId, 50 | }; 51 | 52 | const imageUrls = []; 53 | 54 | for (const imageFile of images) { 55 | const imageBuffer = await imageFile.arrayBuffer(); 56 | const imageArray = Array.from(new Uint8Array(imageBuffer)); 57 | const imageData = Buffer.from(imageArray); 58 | 59 | // Convert the image data to base64 60 | const imageBase64 = imageData.toString('base64'); 61 | 62 | // Make request to upload to Cloudinary 63 | const result = await cloudinary.uploader.upload( 64 | `data:image/png;base64,${imageBase64}`, 65 | { 66 | folder: 'propertypulse', 67 | } 68 | ); 69 | 70 | imageUrls.push(result.secure_url); 71 | } 72 | 73 | propertyData.images = imageUrls; 74 | 75 | const newProperty = new Property(propertyData); 76 | await newProperty.save(); 77 | 78 | revalidatePath('/', 'layout'); 79 | 80 | redirect(`/properties/${newProperty._id}`); 81 | } 82 | 83 | export default addProperty; 84 | -------------------------------------------------------------------------------- /app/actions/bookmarkProperty.js: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import connectDB from '@/config/database'; 4 | import User from '@/models/User'; 5 | import { getSessionUser } from '@/utils/getSessionUser'; 6 | import { revalidatePath } from 'next/cache'; 7 | 8 | async function bookmarkProperty(propertyId) { 9 | await connectDB(); 10 | 11 | const sessionUser = await getSessionUser(); 12 | 13 | if (!sessionUser || !sessionUser.userId) { 14 | return { error: 'User ID is required' }; 15 | } 16 | 17 | const { userId } = sessionUser; 18 | 19 | // Find user in database 20 | const user = await User.findById(userId); 21 | 22 | // Check if property is bookmarked 23 | let isBookmarked = user.bookmarks.includes(propertyId); 24 | console.log(isBookmarked); 25 | 26 | let message; 27 | 28 | if (isBookmarked) { 29 | // If already bookmarked, remove it 30 | user.bookmarks.pull(propertyId); 31 | message = 'Bookmark removed successfully'; 32 | isBookmarked = false; 33 | } else { 34 | // If not bookmarked, add it 35 | user.bookmarks.push(propertyId); 36 | message = 'Bookmark added successfully'; 37 | isBookmarked = true; 38 | } 39 | 40 | console.log(message); 41 | 42 | await user.save(); 43 | revalidatePath('/properties/saved', 'page'); 44 | 45 | return { message, isBookmarked }; 46 | } 47 | 48 | export default bookmarkProperty; 49 | -------------------------------------------------------------------------------- /app/actions/checkBookmarkStatus.js: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | const { default: connectDB } = require('@/config/database'); 4 | const { default: User } = require('@/models/User'); 5 | const { getSessionUser } = require('@/utils/getSessionUser'); 6 | 7 | async function checkBookmarkStatus(propertyId) { 8 | await connectDB(); 9 | 10 | const sessionUser = await getSessionUser(); 11 | 12 | if (!sessionUser || !sessionUser.userId) { 13 | return { error: 'User ID is required' }; 14 | } 15 | 16 | const { userId } = sessionUser; 17 | 18 | // Find user in database 19 | const user = await User.findById(userId); 20 | 21 | // Check if property is bookmarked 22 | let isBookmarked = user.bookmarks.includes(propertyId); 23 | 24 | return { isBookmarked }; 25 | } 26 | 27 | export default checkBookmarkStatus; 28 | -------------------------------------------------------------------------------- /app/actions/deleteMessage.js: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import connectDB from '@/config/database'; 4 | import Message from '@/models/Message'; 5 | import { getSessionUser } from '@/utils/getSessionUser'; 6 | import { revalidatePath } from 'next/cache'; 7 | 8 | async function deleteMessage(messageId) { 9 | await connectDB(); 10 | 11 | const sessionUser = await getSessionUser(); 12 | 13 | if (!sessionUser || !sessionUser.user) { 14 | throw new Error('User ID is required'); 15 | } 16 | 17 | const { userId } = sessionUser; 18 | 19 | const message = await Message.findById(messageId); 20 | 21 | if (!message) throw new Error('Message Not Found'); 22 | 23 | // Verify ownership 24 | if (message.recipient.toString() !== userId) { 25 | throw new Error('Unauthorized'); 26 | } 27 | 28 | // revalidate cache 29 | revalidatePath('/messages', 'page'); 30 | 31 | await message.deleteOne(); 32 | } 33 | 34 | export default deleteMessage; 35 | -------------------------------------------------------------------------------- /app/actions/deleteProperty.js: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | import cloudinary from '@/config/cloudinary'; 3 | import connectDB from '@/config/database'; 4 | import Property from '@/models/Property'; 5 | import { getSessionUser } from '@/utils/getSessionUser'; 6 | import { revalidatePath } from 'next/cache'; 7 | 8 | async function deleteProperty(propertyId) { 9 | const sessionUser = await getSessionUser(); 10 | 11 | // Check for session 12 | if (!sessionUser || !sessionUser.userId) { 13 | throw new Error('User ID is required'); 14 | } 15 | 16 | const { userId } = sessionUser; 17 | 18 | await connectDB(); 19 | 20 | const property = await Property.findById(propertyId); 21 | 22 | if (!property) throw new Error('Property Not Found'); 23 | 24 | // Verify ownership 25 | if (property.owner.toString() !== userId) { 26 | throw new Error('Unauthorized'); 27 | } 28 | 29 | // extract public id's from image url in DB 30 | const publicIds = property.images.map((imageUrl) => { 31 | const parts = imageUrl.split('/'); 32 | return parts.at(-1).split('.').at(0); 33 | }); 34 | 35 | // Delete images from Cloudinary 36 | if (publicIds.length > 0) { 37 | for (let publicId of publicIds) { 38 | await cloudinary.uploader.destroy('propertypulse/' + publicId); 39 | } 40 | } 41 | 42 | // Proceed with property deletion 43 | await property.deleteOne(); 44 | 45 | revalidatePath('/', 'layout'); 46 | } 47 | 48 | export default deleteProperty; 49 | -------------------------------------------------------------------------------- /app/actions/getUnreadMessageCount.js: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import connectDB from '@/config/database'; 4 | import Message from '@/models/Message'; 5 | import { getSessionUser } from '@/utils/getSessionUser'; 6 | 7 | async function getUnreadMessageCount() { 8 | await connectDB(); 9 | 10 | const sessionUser = await getSessionUser(); 11 | 12 | if (!sessionUser || !sessionUser.user) { 13 | return { error: 'User ID is required' }; 14 | } 15 | 16 | const { userId } = sessionUser; 17 | 18 | const count = await Message.countDocuments({ 19 | recipient: userId, 20 | read: false, 21 | }); 22 | 23 | return { count }; 24 | } 25 | 26 | export default getUnreadMessageCount; 27 | -------------------------------------------------------------------------------- /app/actions/markMessageAsRead.js: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | import connectDB from '@/config/database'; 3 | import Message from '@/models/Message'; 4 | import { getSessionUser } from '@/utils/getSessionUser'; 5 | import { revalidatePath } from 'next/cache'; 6 | 7 | async function markMessageAsRead(messageId) { 8 | await connectDB(); 9 | 10 | const sessionUser = await getSessionUser(); 11 | 12 | if (!sessionUser || !sessionUser.user) { 13 | throw new Error('User ID is required'); 14 | } 15 | 16 | const { userId } = sessionUser; 17 | 18 | const message = await Message.findById(messageId); 19 | 20 | if (!message) throw new Error('Message not found'); 21 | 22 | // Verify ownership 23 | if (message.recipient.toString() !== userId) { 24 | return new Response('Unauthorized', { status: 401 }); 25 | } 26 | 27 | message.read = !message.read; 28 | 29 | revalidatePath('/messages', 'page'); 30 | 31 | await message.save(); 32 | 33 | return message.read; 34 | } 35 | 36 | export default markMessageAsRead; 37 | -------------------------------------------------------------------------------- /app/actions/updateProperty.js: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import connectDB from '@/config/database'; 4 | import Property from '@/models/Property'; 5 | import { getSessionUser } from '@/utils/getSessionUser'; 6 | import { revalidatePath } from 'next/cache'; 7 | import { redirect } from 'next/navigation'; 8 | 9 | async function updateProperty(propertyId, formData) { 10 | await connectDB(); 11 | 12 | const sessionUser = await getSessionUser(); 13 | 14 | const { userId } = sessionUser; 15 | 16 | const existingProperty = await Property.findById(propertyId); 17 | 18 | // Verify ownership 19 | if (existingProperty.owner.toString() !== userId) { 20 | throw new Error('Current user does not own this property.'); 21 | } 22 | 23 | const propertyData = { 24 | type: formData.get('type'), 25 | name: formData.get('name'), 26 | description: formData.get('description'), 27 | location: { 28 | street: formData.get('location.street'), 29 | city: formData.get('location.city'), 30 | state: formData.get('location.state'), 31 | zipcode: formData.get('location.zipcode'), 32 | }, 33 | beds: formData.get('beds'), 34 | baths: formData.get('baths'), 35 | square_feet: formData.get('square_feet'), 36 | amenities: formData.getAll('amenities'), 37 | rates: { 38 | weekly: formData.get('rates.weekly'), 39 | monthly: formData.get('rates.monthly'), 40 | nightly: formData.get('rates.nightly.'), 41 | }, 42 | seller_info: { 43 | name: formData.get('seller_info.name'), 44 | email: formData.get('seller_info.email'), 45 | phone: formData.get('seller_info.phone'), 46 | }, 47 | owner: userId, 48 | }; 49 | 50 | const updatedProperty = await Property.findByIdAndUpdate( 51 | propertyId, 52 | propertyData 53 | ); 54 | 55 | revalidatePath('/', 'layout'); 56 | 57 | redirect(`/properties/${updatedProperty._id}`); 58 | } 59 | 60 | export default updateProperty; 61 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.js: -------------------------------------------------------------------------------- 1 | import { authOptions } from '@/utils/authOptions'; 2 | import NextAuth from 'next-auth/next'; 3 | 4 | const handler = NextAuth(authOptions); 5 | 6 | export { handler as GET, handler as POST }; 7 | -------------------------------------------------------------------------------- /app/error.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { FaExclamationCircle } from 'react-icons/fa'; 3 | import Link from 'next/link'; 4 | 5 | const ErrorPage = ({ error, reset }) => { 6 | console.log(error); 7 | return ( 8 |
9 |
10 |
11 |
12 | 13 |
14 |
15 |

16 | Something Went Wrong 17 |

18 |

{error.toString()}

19 | 23 | Go Home 24 | 25 |
26 |
27 |
28 |
29 |
30 | ); 31 | }; 32 | 33 | export default ErrorPage; 34 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syedrizvinet/nextjs-property-pulse/64cd9ab879525c7d55ca7de838d0cee96930c400/app/favicon.ico -------------------------------------------------------------------------------- /app/layout.jsx: -------------------------------------------------------------------------------- 1 | import Navbar from '@/components/Navbar'; 2 | import Footer from '@/components/Footer'; 3 | import AuthProvider from '@/components/AuthProvider'; 4 | import { GlobalProvider } from '@/context/GlobalContext'; 5 | import { ToastContainer } from 'react-toastify'; 6 | import 'react-toastify/dist/ReactToastify.css'; 7 | import '@/assets/styles/globals.css'; 8 | import 'photoswipe/dist/photoswipe.css'; 9 | 10 | export const metadata = { 11 | title: 'PropertyPulse', 12 | description: 'Find The Perfect Rental Property', 13 | keywords: 'rental, property, real estate', 14 | }; 15 | 16 | const MainLayout = ({ children }) => { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 |
{children}
24 |