├── .gitignore ├── client ├── README.md ├── components.json ├── eslint.config.mjs ├── next-env.d.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public │ ├── landing-call-to-action.jpg │ ├── landing-discover-bg.jpg │ ├── landing-i1.png │ ├── landing-i2.png │ ├── landing-i3.png │ ├── landing-i4.png │ ├── landing-i5.png │ ├── landing-i6.png │ ├── landing-i7.png │ ├── landing-icon-calendar.png │ ├── landing-icon-heart.png │ ├── landing-icon-wand.png │ ├── landing-search1.png │ ├── landing-search2.png │ ├── landing-search3.png │ ├── landing-splash.jpg │ ├── logo.svg │ ├── placeholder.jpg │ ├── singlelisting-2.jpg │ └── singlelisting-3.jpg ├── src │ ├── app │ │ ├── (auth) │ │ │ └── authProvider.tsx │ │ ├── (dashboard) │ │ │ ├── layout.tsx │ │ │ ├── managers │ │ │ │ ├── applications │ │ │ │ │ └── page.tsx │ │ │ │ ├── newproperty │ │ │ │ │ └── page.tsx │ │ │ │ ├── properties │ │ │ │ │ ├── [id] │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── settings │ │ │ │ │ └── page.tsx │ │ │ └── tenants │ │ │ │ ├── applications │ │ │ │ └── page.tsx │ │ │ │ ├── favorites │ │ │ │ └── page.tsx │ │ │ │ ├── residences │ │ │ │ ├── [id] │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ │ └── settings │ │ │ │ └── page.tsx │ │ ├── (nondashboard) │ │ │ ├── landing │ │ │ │ ├── CallToActionSection.tsx │ │ │ │ ├── DiscoverSection.tsx │ │ │ │ ├── FeaturesSection.tsx │ │ │ │ ├── FooterSection.tsx │ │ │ │ ├── HeroSection.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── search │ │ │ │ ├── FiltersBar.tsx │ │ │ │ ├── FiltersFull.tsx │ │ │ │ ├── Listings.tsx │ │ │ │ ├── Map.tsx │ │ │ │ ├── [id] │ │ │ │ ├── ApplicationModal.tsx │ │ │ │ ├── ContactWidget.tsx │ │ │ │ ├── ImagePreviews.tsx │ │ │ │ ├── PropertyDetails.tsx │ │ │ │ ├── PropertyLocation.tsx │ │ │ │ ├── PropertyOverview.tsx │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── providers.tsx │ ├── components │ │ ├── AppSidebar.tsx │ │ ├── ApplicationCard.tsx │ │ ├── Card.tsx │ │ ├── CardCompact.tsx │ │ ├── FormField.tsx │ │ ├── Header.tsx │ │ ├── Loading.tsx │ │ ├── Navbar.tsx │ │ ├── SettingsForm.tsx │ │ └── ui │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── command.tsx │ │ │ ├── dialog.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── textarea.tsx │ │ │ └── tooltip.tsx │ ├── hooks │ │ └── use-mobile.tsx │ ├── lib │ │ ├── constants.ts │ │ ├── schemas.ts │ │ └── utils.ts │ ├── state │ │ ├── api.ts │ │ ├── index.ts │ │ └── redux.tsx │ └── types │ │ ├── index.d.ts │ │ └── prismaTypes.d.ts ├── tailwind.config.ts └── tsconfig.json └── server ├── .gitignore ├── aws-ec2-instructions.md ├── ecosystem.config.js ├── package-lock.json ├── package.json ├── prisma ├── migrations │ ├── 20250203030418_init │ │ └── migration.sql │ └── migration_lock.toml ├── schema.prisma ├── seed.ts └── seedData │ ├── application.json │ ├── lease.json │ ├── location.json │ ├── manager.json │ ├── payment.json │ ├── property.json │ └── tenant.json ├── src ├── controllers │ ├── applicationControllers.ts │ ├── leaseControllers.ts │ ├── managerControllers.ts │ ├── propertyControllers.ts │ └── tenantControllers.ts ├── index.ts ├── middleware │ └── authMiddleware.ts └── routes │ ├── applicationRoutes.ts │ ├── leaseRoutes.ts │ ├── managerRoutes.ts │ ├── propertyRoutes.ts │ └── tenantRoutes.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/dist/ 3 | 4 | .env 5 | client/.env.local 6 | client/.env.development 7 | client/.env.production 8 | server/.env 9 | 10 | client/.next/ 11 | 12 | .DS_Store 13 | Thumbs.db 14 | 15 | .vscode/ 16 | 17 | client/node_modules/ 18 | server/node_modules/ 19 | client/dist/ 20 | server/dist/ 21 | client/.env 22 | server/.env 23 | client/.next/ 24 | node_modules -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /client/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /client/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | { 15 | rules: { 16 | "@typescript-eslint/no-explicit-any": "off", 17 | "@typescript-eslint/no-unused-vars": "off", 18 | "@typescript-eslint/no-empty-object-type": "off", 19 | "@typescript-eslint/no-unnecessary-type-constraint": "off", 20 | "@typescript-eslint/no-wrapper-object-types": "off", 21 | "@typescript-eslint/no-unsafe-function-type": "off", 22 | }, 23 | }, 24 | ]; 25 | 26 | export default eslintConfig; 27 | -------------------------------------------------------------------------------- /client/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /client/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | images: { 5 | remotePatterns: [ 6 | { 7 | protocol: "https", 8 | hostname: "example.com", 9 | port: "", 10 | pathname: "/**", 11 | }, 12 | { 13 | protocol: "https", 14 | hostname: "*.amazonaws.com", 15 | port: "", 16 | pathname: "/**", 17 | }, 18 | ], 19 | }, 20 | }; 21 | 22 | export default nextConfig; 23 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "real-estate", 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 | "@aws-amplify/ui-react": "^6.9.1", 13 | "@fortawesome/fontawesome-svg-core": "^6.7.2", 14 | "@fortawesome/free-brands-svg-icons": "^6.7.2", 15 | "@fortawesome/react-fontawesome": "^0.2.2", 16 | "@hookform/resolvers": "^3.10.0", 17 | "@radix-ui/react-avatar": "^1.1.2", 18 | "@radix-ui/react-checkbox": "^1.1.3", 19 | "@radix-ui/react-dialog": "^1.1.5", 20 | "@radix-ui/react-dropdown-menu": "^2.1.5", 21 | "@radix-ui/react-label": "^2.1.1", 22 | "@radix-ui/react-navigation-menu": "^1.2.4", 23 | "@radix-ui/react-radio-group": "^1.2.2", 24 | "@radix-ui/react-select": "^2.1.5", 25 | "@radix-ui/react-separator": "^1.1.1", 26 | "@radix-ui/react-slider": "^1.2.2", 27 | "@radix-ui/react-slot": "^1.1.1", 28 | "@radix-ui/react-switch": "^1.1.2", 29 | "@radix-ui/react-tabs": "^1.1.2", 30 | "@radix-ui/react-tooltip": "^1.1.7", 31 | "@reduxjs/toolkit": "^2.5.1", 32 | "aws-amplify": "^6.12.2", 33 | "class-variance-authority": "^0.7.1", 34 | "clsx": "^2.1.1", 35 | "cmdk": "^1.0.0", 36 | "date-fns": "^4.1.0", 37 | "dotenv": "^16.4.7", 38 | "filepond": "^4.32.7", 39 | "filepond-plugin-image-exif-orientation": "^1.0.11", 40 | "filepond-plugin-image-preview": "^4.6.12", 41 | "framer-motion": "^12.0.6", 42 | "lodash": "^4.17.21", 43 | "lucide-react": "^0.474.0", 44 | "mapbox-gl": "^3.9.4", 45 | "next": "15.1.6", 46 | "next-themes": "^0.4.4", 47 | "react": "^19.0.0", 48 | "react-dom": "^19.0.0", 49 | "react-filepond": "^7.1.3", 50 | "react-hook-form": "^7.54.2", 51 | "react-redux": "^9.2.0", 52 | "sonner": "^1.7.4", 53 | "tailwind-merge": "^3.0.1", 54 | "tailwindcss-animate": "^1.0.7", 55 | "zod": "^3.24.1" 56 | }, 57 | "devDependencies": { 58 | "@eslint/eslintrc": "^3", 59 | "@types/lodash": "^4.17.15", 60 | "@types/node": "^20.17.16", 61 | "@types/react": "^19", 62 | "@types/react-dom": "^19", 63 | "@types/uuid": "^10.0.0", 64 | "eslint": "^9", 65 | "eslint-config-next": "15.1.6", 66 | "postcss": "^8", 67 | "tailwindcss": "^3.4.1", 68 | "typescript": "^5" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /client/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /client/public/landing-call-to-action.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/landing-call-to-action.jpg -------------------------------------------------------------------------------- /client/public/landing-discover-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/landing-discover-bg.jpg -------------------------------------------------------------------------------- /client/public/landing-i1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/landing-i1.png -------------------------------------------------------------------------------- /client/public/landing-i2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/landing-i2.png -------------------------------------------------------------------------------- /client/public/landing-i3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/landing-i3.png -------------------------------------------------------------------------------- /client/public/landing-i4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/landing-i4.png -------------------------------------------------------------------------------- /client/public/landing-i5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/landing-i5.png -------------------------------------------------------------------------------- /client/public/landing-i6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/landing-i6.png -------------------------------------------------------------------------------- /client/public/landing-i7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/landing-i7.png -------------------------------------------------------------------------------- /client/public/landing-icon-calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/landing-icon-calendar.png -------------------------------------------------------------------------------- /client/public/landing-icon-heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/landing-icon-heart.png -------------------------------------------------------------------------------- /client/public/landing-icon-wand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/landing-icon-wand.png -------------------------------------------------------------------------------- /client/public/landing-search1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/landing-search1.png -------------------------------------------------------------------------------- /client/public/landing-search2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/landing-search2.png -------------------------------------------------------------------------------- /client/public/landing-search3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/landing-search3.png -------------------------------------------------------------------------------- /client/public/landing-splash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/landing-splash.jpg -------------------------------------------------------------------------------- /client/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/public/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/placeholder.jpg -------------------------------------------------------------------------------- /client/public/singlelisting-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/singlelisting-2.jpg -------------------------------------------------------------------------------- /client/public/singlelisting-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/public/singlelisting-3.jpg -------------------------------------------------------------------------------- /client/src/app/(auth)/authProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useEffect } from "react"; 4 | import { Amplify } from "aws-amplify"; 5 | import { 6 | Authenticator, 7 | Heading, 8 | Radio, 9 | RadioGroupField, 10 | useAuthenticator, 11 | View, 12 | } from "@aws-amplify/ui-react"; 13 | import "@aws-amplify/ui-react/styles.css"; 14 | import { useRouter, usePathname } from "next/navigation"; 15 | 16 | // https://docs.amplify.aws/gen1/javascript/tools/libraries/configure-categories/ 17 | Amplify.configure({ 18 | Auth: { 19 | Cognito: { 20 | userPoolId: process.env.NEXT_PUBLIC_AWS_COGNITO_USER_POOL_ID!, 21 | userPoolClientId: 22 | process.env.NEXT_PUBLIC_AWS_COGNITO_USER_POOL_CLIENT_ID!, 23 | }, 24 | }, 25 | }); 26 | 27 | const components = { 28 | Header() { 29 | return ( 30 | 31 | 32 | RENT 33 | 34 | IFUL 35 | 36 | 37 |

38 | Welcome! Please sign in to continue 39 |

40 |
41 | ); 42 | }, 43 | SignIn: { 44 | Footer() { 45 | const { toSignUp } = useAuthenticator(); 46 | return ( 47 | 48 |

49 | Don't have an account?{" "} 50 | 56 |

57 |
58 | ); 59 | }, 60 | }, 61 | SignUp: { 62 | FormFields() { 63 | const { validationErrors } = useAuthenticator(); 64 | 65 | return ( 66 | <> 67 | 68 | 75 | Tenant 76 | Manager 77 | 78 | 79 | ); 80 | }, 81 | 82 | Footer() { 83 | const { toSignIn } = useAuthenticator(); 84 | return ( 85 | 86 |

87 | Already have an account?{" "} 88 | 94 |

95 |
96 | ); 97 | }, 98 | }, 99 | }; 100 | 101 | const formFields = { 102 | signIn: { 103 | username: { 104 | placeholder: "Enter your email", 105 | label: "Email", 106 | isRequired: true, 107 | }, 108 | password: { 109 | placeholder: "Enter your password", 110 | label: "Password", 111 | isRequired: true, 112 | }, 113 | }, 114 | signUp: { 115 | username: { 116 | order: 1, 117 | placeholder: "Choose a username", 118 | label: "Username", 119 | isRequired: true, 120 | }, 121 | email: { 122 | order: 2, 123 | placeholder: "Enter your email address", 124 | label: "Email", 125 | isRequired: true, 126 | }, 127 | password: { 128 | order: 3, 129 | placeholder: "Create a password", 130 | label: "Password", 131 | isRequired: true, 132 | }, 133 | confirm_password: { 134 | order: 4, 135 | placeholder: "Confirm your password", 136 | label: "Confirm Password", 137 | isRequired: true, 138 | }, 139 | }, 140 | }; 141 | 142 | const Auth = ({ children }: { children: React.ReactNode }) => { 143 | const { user } = useAuthenticator((context) => [context.user]); 144 | const router = useRouter(); 145 | const pathname = usePathname(); 146 | 147 | const isAuthPage = pathname.match(/^\/(signin|signup)$/); 148 | const isDashboardPage = 149 | pathname.startsWith("/manager") || pathname.startsWith("/tenants"); 150 | 151 | // Redirect authenticated users away from auth pages 152 | useEffect(() => { 153 | if (user && isAuthPage) { 154 | router.push("/"); 155 | } 156 | }, [user, isAuthPage, router]); 157 | 158 | // Allow access to public pages without authentication 159 | if (!isAuthPage && !isDashboardPage) { 160 | return <>{children}; 161 | } 162 | 163 | return ( 164 |
165 | 170 | {() => <>{children}} 171 | 172 |
173 | ); 174 | }; 175 | 176 | export default Auth; 177 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Navbar from "@/components/Navbar"; 4 | import { SidebarProvider } from "@/components/ui/sidebar"; 5 | import Sidebar from "@/components/AppSidebar"; 6 | import { NAVBAR_HEIGHT } from "@/lib/constants"; 7 | import React, { useEffect, useState } from "react"; 8 | import { useGetAuthUserQuery } from "@/state/api"; 9 | import { usePathname, useRouter } from "next/navigation"; 10 | 11 | const DashboardLayout = ({ children }: { children: React.ReactNode }) => { 12 | const { data: authUser, isLoading: authLoading } = useGetAuthUserQuery(); 13 | const router = useRouter(); 14 | const pathname = usePathname(); 15 | const [isLoading, setIsLoading] = useState(true); 16 | 17 | useEffect(() => { 18 | if (authUser) { 19 | const userRole = authUser.userRole?.toLowerCase(); 20 | if ( 21 | (userRole === "manager" && pathname.startsWith("/tenants")) || 22 | (userRole === "tenant" && pathname.startsWith("/managers")) 23 | ) { 24 | router.push( 25 | userRole === "manager" 26 | ? "/managers/properties" 27 | : "/tenants/favorites", 28 | { scroll: false } 29 | ); 30 | } else { 31 | setIsLoading(false); 32 | } 33 | } 34 | }, [authUser, router, pathname]); 35 | 36 | if (authLoading || isLoading) return <>Loading...; 37 | if (!authUser?.userRole) return null; 38 | 39 | return ( 40 | 41 |
42 | 43 |
44 |
45 | 46 |
47 | {children} 48 |
49 |
50 |
51 |
52 |
53 | ); 54 | }; 55 | 56 | export default DashboardLayout; 57 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/managers/properties/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Card from "@/components/Card"; 4 | import Header from "@/components/Header"; 5 | import Loading from "@/components/Loading"; 6 | import { useGetAuthUserQuery, useGetManagerPropertiesQuery } from "@/state/api"; 7 | import React from "react"; 8 | 9 | const Properties = () => { 10 | const { data: authUser } = useGetAuthUserQuery(); 11 | const { 12 | data: managerProperties, 13 | isLoading, 14 | error, 15 | } = useGetManagerPropertiesQuery(authUser?.cognitoInfo?.userId || "", { 16 | skip: !authUser?.cognitoInfo?.userId, 17 | }); 18 | 19 | if (isLoading) return ; 20 | if (error) return
Error loading manager properties
; 21 | 22 | return ( 23 |
24 |
28 |
29 | {managerProperties?.map((property) => ( 30 | {}} 35 | showFavoriteButton={false} 36 | propertyLink={`/managers/properties/${property.id}`} 37 | /> 38 | ))} 39 |
40 | {(!managerProperties || managerProperties.length === 0) && ( 41 |

You don‘t manage any properties

42 | )} 43 |
44 | ); 45 | }; 46 | 47 | export default Properties; 48 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/managers/settings/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import SettingsForm from "@/components/SettingsForm"; 4 | import { 5 | useGetAuthUserQuery, 6 | useUpdateManagerSettingsMutation, 7 | } from "@/state/api"; 8 | import React from "react"; 9 | 10 | const ManagerSettings = () => { 11 | const { data: authUser, isLoading } = useGetAuthUserQuery(); 12 | const [updateManager] = useUpdateManagerSettingsMutation(); 13 | 14 | if (isLoading) return <>Loading...; 15 | 16 | const initialData = { 17 | name: authUser?.userInfo.name, 18 | email: authUser?.userInfo.email, 19 | phoneNumber: authUser?.userInfo.phoneNumber, 20 | }; 21 | 22 | const handleSubmit = async (data: typeof initialData) => { 23 | await updateManager({ 24 | cognitoId: authUser?.cognitoInfo?.userId, 25 | ...data, 26 | }); 27 | }; 28 | 29 | return ( 30 | 35 | ); 36 | }; 37 | 38 | export default ManagerSettings; 39 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/tenants/applications/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import ApplicationCard from "@/components/ApplicationCard"; 4 | import Header from "@/components/Header"; 5 | import Loading from "@/components/Loading"; 6 | import { useGetApplicationsQuery, useGetAuthUserQuery } from "@/state/api"; 7 | import { CircleCheckBig, Clock, Download, XCircle } from "lucide-react"; 8 | import React from "react"; 9 | 10 | const Applications = () => { 11 | const { data: authUser } = useGetAuthUserQuery(); 12 | const { 13 | data: applications, 14 | isLoading, 15 | isError, 16 | } = useGetApplicationsQuery({ 17 | userId: authUser?.cognitoInfo?.userId, 18 | userType: "tenant", 19 | }); 20 | 21 | if (isLoading) return ; 22 | if (isError || !applications) return
Error fetching applications
; 23 | 24 | return ( 25 |
26 |
30 |
31 | {applications?.map((application) => ( 32 | 37 |
38 | {application.status === "Approved" ? ( 39 |
40 | 41 | The property is being rented by you until{" "} 42 | {new Date(application.lease?.endDate).toLocaleDateString()} 43 |
44 | ) : application.status === "Pending" ? ( 45 |
46 | 47 | Your application is pending approval 48 |
49 | ) : ( 50 |
51 | 52 | Your application has been denied 53 |
54 | )} 55 | 56 | 63 |
64 |
65 | ))} 66 |
67 |
68 | ); 69 | }; 70 | 71 | export default Applications; 72 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/tenants/favorites/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Card from "@/components/Card"; 4 | import Header from "@/components/Header"; 5 | import Loading from "@/components/Loading"; 6 | import { 7 | useGetAuthUserQuery, 8 | useGetPropertiesQuery, 9 | useGetTenantQuery, 10 | } from "@/state/api"; 11 | import React from "react"; 12 | 13 | const Favorites = () => { 14 | const { data: authUser } = useGetAuthUserQuery(); 15 | const { data: tenant } = useGetTenantQuery( 16 | authUser?.cognitoInfo?.userId || "", 17 | { 18 | skip: !authUser?.cognitoInfo?.userId, 19 | } 20 | ); 21 | 22 | const { 23 | data: favoriteProperties, 24 | isLoading, 25 | error, 26 | } = useGetPropertiesQuery( 27 | { favoriteIds: tenant?.favorites?.map((fav: { id: number }) => fav.id) }, 28 | { skip: !tenant?.favorites || tenant?.favorites.length === 0 } 29 | ); 30 | 31 | if (isLoading) return ; 32 | if (error) return
Error loading favorites
; 33 | 34 | return ( 35 |
36 |
40 |
41 | {favoriteProperties?.map((property) => ( 42 | {}} 47 | showFavoriteButton={false} 48 | propertyLink={`/tenants/residences/${property.id}`} 49 | /> 50 | ))} 51 |
52 | {(!favoriteProperties || favoriteProperties.length === 0) && ( 53 |

You don‘t have any favorited properties

54 | )} 55 |
56 | ); 57 | }; 58 | 59 | export default Favorites; 60 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/tenants/residences/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Card from "@/components/Card"; 4 | import Header from "@/components/Header"; 5 | import Loading from "@/components/Loading"; 6 | import { 7 | useGetAuthUserQuery, 8 | useGetCurrentResidencesQuery, 9 | useGetTenantQuery, 10 | } from "@/state/api"; 11 | import React from "react"; 12 | 13 | const Residences = () => { 14 | const { data: authUser } = useGetAuthUserQuery(); 15 | const { data: tenant } = useGetTenantQuery( 16 | authUser?.cognitoInfo?.userId || "", 17 | { 18 | skip: !authUser?.cognitoInfo?.userId, 19 | } 20 | ); 21 | 22 | const { 23 | data: currentResidences, 24 | isLoading, 25 | error, 26 | } = useGetCurrentResidencesQuery(authUser?.cognitoInfo?.userId || "", { 27 | skip: !authUser?.cognitoInfo?.userId, 28 | }); 29 | 30 | if (isLoading) return ; 31 | if (error) return
Error loading current residences
; 32 | 33 | return ( 34 |
35 |
39 |
40 | {currentResidences?.map((property) => ( 41 | {}} 46 | showFavoriteButton={false} 47 | propertyLink={`/tenants/residences/${property.id}`} 48 | /> 49 | ))} 50 |
51 | {(!currentResidences || currentResidences.length === 0) && ( 52 |

You don‘t have any current residences

53 | )} 54 |
55 | ); 56 | }; 57 | 58 | export default Residences; 59 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/tenants/settings/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import SettingsForm from "@/components/SettingsForm"; 4 | import { 5 | useGetAuthUserQuery, 6 | useUpdateTenantSettingsMutation, 7 | } from "@/state/api"; 8 | import React from "react"; 9 | 10 | const TenantSettings = () => { 11 | const { data: authUser, isLoading } = useGetAuthUserQuery(); 12 | const [updateTenant] = useUpdateTenantSettingsMutation(); 13 | 14 | if (isLoading) return <>Loading...; 15 | 16 | const initialData = { 17 | name: authUser?.userInfo.name, 18 | email: authUser?.userInfo.email, 19 | phoneNumber: authUser?.userInfo.phoneNumber, 20 | }; 21 | 22 | const handleSubmit = async (data: typeof initialData) => { 23 | await updateTenant({ 24 | cognitoId: authUser?.cognitoInfo?.userId, 25 | ...data, 26 | }); 27 | }; 28 | 29 | return ( 30 | 35 | ); 36 | }; 37 | 38 | export default TenantSettings; 39 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/landing/CallToActionSection.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import React from "react"; 5 | import { motion } from "framer-motion"; 6 | import Link from "next/link"; 7 | 8 | const CallToActionSection = () => { 9 | return ( 10 |
11 | Rentiful Search Section Background 17 |
18 | 25 |
26 |
27 |

28 | Find Your Dream Rental Property 29 |

30 |
31 |
32 |

33 | Discover a wide range of rental properties in your desired 34 | location. 35 |

36 |
37 | 43 | 48 | Sign Up 49 | 50 |
51 |
52 |
53 |
54 |
55 | ); 56 | }; 57 | 58 | export default CallToActionSection; 59 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/landing/DiscoverSection.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { motion } from "framer-motion"; 5 | import Image from "next/image"; 6 | 7 | const containerVariants = { 8 | hidden: { opacity: 0 }, 9 | visible: { 10 | opacity: 1, 11 | transition: { 12 | staggerChildren: 0.2, 13 | }, 14 | }, 15 | }; 16 | 17 | const itemVariants = { 18 | hidden: { opacity: 0, y: 20 }, 19 | visible: { opacity: 1, y: 0 }, 20 | }; 21 | 22 | const DiscoverSection = () => { 23 | return ( 24 | 31 |
32 | 33 |

34 | Discover 35 |

36 |

37 | Find your Dream Rental Property Today! 38 |

39 |

40 | Searching for your dream rental property has never been easier. With 41 | our user-friendly search feature, you can quickly find the perfect 42 | home that meets all your needs. Start your search today and discover 43 | your dream rental property! 44 |

45 |
46 |
47 | {[ 48 | { 49 | imageSrc: "/landing-icon-wand.png", 50 | title: "Search for Properties", 51 | description: 52 | "Browse through our extensive collection of rental properties in your desired location.", 53 | }, 54 | { 55 | imageSrc: "/landing-icon-calendar.png", 56 | title: "Book Your Rental", 57 | description: 58 | "Once you've found the perfect rental property, easily book it online with just a few clicks.", 59 | }, 60 | { 61 | imageSrc: "/landing-icon-heart.png", 62 | title: "Enjoy your New Home", 63 | description: 64 | "Move into your new rental property and start enjoying your dream home.", 65 | }, 66 | ].map((card, index) => ( 67 | 68 | 69 | 70 | ))} 71 |
72 |
73 |
74 | ); 75 | }; 76 | 77 | const DiscoverCard = ({ 78 | imageSrc, 79 | title, 80 | description, 81 | }: { 82 | imageSrc: string; 83 | title: string; 84 | description: string; 85 | }) => ( 86 |
87 |
88 | {title} 95 |
96 |

{title}

97 |

{description}

98 |
99 | ); 100 | 101 | export default DiscoverSection; 102 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/landing/FeaturesSection.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { motion } from "framer-motion"; 5 | import Image from "next/image"; 6 | import Link from "next/link"; 7 | 8 | const containerVariants = { 9 | hidden: { opacity: 0, y: 50 }, 10 | visible: { 11 | opacity: 1, 12 | y: 0, 13 | transition: { 14 | duration: 0.5, 15 | staggerChildren: 0.2, 16 | }, 17 | }, 18 | }; 19 | 20 | const itemVariants = { 21 | hidden: { opacity: 0, y: 20 }, 22 | visible: { opacity: 1, y: 0 }, 23 | }; 24 | 25 | const FeaturesSection = () => { 26 | return ( 27 | 34 |
35 | 39 | Quickly find the home you want using our effective search filters! 40 | 41 |
42 | {[0, 1, 2].map((index) => ( 43 | 44 | 63 | 64 | ))} 65 |
66 |
67 |
68 | ); 69 | }; 70 | 71 | const FeatureCard = ({ 72 | imageSrc, 73 | title, 74 | description, 75 | linkText, 76 | linkHref, 77 | }: { 78 | imageSrc: string; 79 | title: string; 80 | description: string; 81 | linkText: string; 82 | linkHref: string; 83 | }) => ( 84 |
85 |
86 | {title} 93 |
94 |

{title}

95 |

{description}

96 | 101 | {linkText} 102 | 103 |
104 | ); 105 | 106 | export default FeaturesSection; 107 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/landing/FooterSection.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import { 5 | faFacebook, 6 | faInstagram, 7 | faTwitter, 8 | faLinkedin, 9 | faYoutube, 10 | } from "@fortawesome/free-brands-svg-icons"; 11 | 12 | const FooterSection = () => { 13 | return ( 14 |
15 |
16 |
17 |
18 | 19 | RENTIFUL 20 | 21 |
22 | 41 | 70 |
71 |
72 | © RENTiful. All rights reserved. 73 | Privacy Policy 74 | Terms of Service 75 | Cookie Policy 76 |
77 |
78 |
79 | ); 80 | }; 81 | 82 | export default FooterSection; 83 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/landing/HeroSection.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import React, { useState } from "react"; 5 | import { motion } from "framer-motion"; 6 | import { Input } from "@/components/ui/input"; 7 | import { Button } from "@/components/ui/button"; 8 | import { useDispatch } from "react-redux"; 9 | import { useRouter } from "next/navigation"; 10 | import { setFilters } from "@/state"; 11 | 12 | const HeroSection = () => { 13 | const dispatch = useDispatch(); 14 | const [searchQuery, setSearchQuery] = useState(""); 15 | const router = useRouter(); 16 | 17 | const handleLocationSearch = async () => { 18 | try { 19 | const trimmedQuery = searchQuery.trim(); 20 | if (!trimmedQuery) return; 21 | 22 | const response = await fetch( 23 | `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent( 24 | trimmedQuery 25 | )}.json?access_token=${ 26 | process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN 27 | }&fuzzyMatch=true` 28 | ); 29 | const data = await response.json(); 30 | if (data.features && data.features.length > 0) { 31 | const [lng, lat] = data.features[0].center; 32 | dispatch( 33 | setFilters({ 34 | location: trimmedQuery, 35 | coordinates: [lat, lng], 36 | }) 37 | ); 38 | const params = new URLSearchParams({ 39 | location: trimmedQuery, 40 | lat: lat.toString(), 41 | lng: lng, 42 | }); 43 | router.push(`/search?${params.toString()}`); 44 | } 45 | } catch (error) { 46 | console.error("error search location:", error); 47 | } 48 | }; 49 | 50 | return ( 51 |
52 | Rentiful Rental Platform Hero Section 59 |
60 | 66 |
67 |

68 | Start your journey to finding the perfect place to call home 69 |

70 |

71 | Explore our wide range of rental properties tailored to fit your 72 | lifestyle and needs! 73 |

74 | 75 |
76 | setSearchQuery(e.target.value)} 80 | placeholder="Search by city, neighborhood or address" 81 | className="w-full max-w-lg rounded-none rounded-l-xl border-none bg-white h-12" 82 | /> 83 | 89 |
90 |
91 |
92 |
93 | ); 94 | }; 95 | 96 | export default HeroSection; 97 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/landing/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import HeroSection from "./HeroSection"; 3 | import FeaturesSection from "./FeaturesSection"; 4 | import DiscoverSection from "./DiscoverSection"; 5 | import CallToActionSection from "./CallToActionSection"; 6 | import FooterSection from "./FooterSection"; 7 | 8 | const Landing = () => { 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default Landing; 21 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Navbar from "@/components/Navbar"; 4 | import { NAVBAR_HEIGHT } from "@/lib/constants"; 5 | import { useGetAuthUserQuery } from "@/state/api"; 6 | import { usePathname, useRouter } from "next/navigation"; 7 | import React, { useEffect, useState } from "react"; 8 | 9 | const Layout = ({ children }: { children: React.ReactNode }) => { 10 | const { data: authUser, isLoading: authLoading } = useGetAuthUserQuery(); 11 | const router = useRouter(); 12 | const pathname = usePathname(); 13 | const [isLoading, setIsLoading] = useState(true); 14 | 15 | useEffect(() => { 16 | if (authUser) { 17 | const userRole = authUser.userRole?.toLowerCase(); 18 | if ( 19 | (userRole === "manager" && pathname.startsWith("/search")) || 20 | (userRole === "manager" && pathname === "/") 21 | ) { 22 | router.push("/managers/properties", { scroll: false }); 23 | } else { 24 | setIsLoading(false); 25 | } 26 | } 27 | }, [authUser, router, pathname]); 28 | 29 | if (authLoading || isLoading) return <>Loading...; 30 | 31 | return ( 32 |
33 | 34 |
38 | {children} 39 |
40 |
41 | ); 42 | }; 43 | 44 | export default Layout; 45 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/search/Listings.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useAddFavoritePropertyMutation, 3 | useGetAuthUserQuery, 4 | useGetPropertiesQuery, 5 | useGetTenantQuery, 6 | useRemoveFavoritePropertyMutation, 7 | } from "@/state/api"; 8 | import { useAppSelector } from "@/state/redux"; 9 | import { Property } from "@/types/prismaTypes"; 10 | import Card from "@/components/Card"; 11 | import React from "react"; 12 | import CardCompact from "@/components/CardCompact"; 13 | 14 | const Listings = () => { 15 | const { data: authUser } = useGetAuthUserQuery(); 16 | const { data: tenant } = useGetTenantQuery( 17 | authUser?.cognitoInfo?.userId || "", 18 | { 19 | skip: !authUser?.cognitoInfo?.userId, 20 | } 21 | ); 22 | const [addFavorite] = useAddFavoritePropertyMutation(); 23 | const [removeFavorite] = useRemoveFavoritePropertyMutation(); 24 | const viewMode = useAppSelector((state) => state.global.viewMode); 25 | const filters = useAppSelector((state) => state.global.filters); 26 | 27 | const { 28 | data: properties, 29 | isLoading, 30 | isError, 31 | } = useGetPropertiesQuery(filters); 32 | 33 | const handleFavoriteToggle = async (propertyId: number) => { 34 | if (!authUser) return; 35 | 36 | const isFavorite = tenant?.favorites?.some( 37 | (fav: Property) => fav.id === propertyId 38 | ); 39 | 40 | if (isFavorite) { 41 | await removeFavorite({ 42 | cognitoId: authUser.cognitoInfo.userId, 43 | propertyId, 44 | }); 45 | } else { 46 | await addFavorite({ 47 | cognitoId: authUser.cognitoInfo.userId, 48 | propertyId, 49 | }); 50 | } 51 | }; 52 | 53 | if (isLoading) return <>Loading...; 54 | if (isError || !properties) return
Failed to fetch properties
; 55 | 56 | return ( 57 |
58 |

59 | {properties.length}{" "} 60 | 61 | Places in {filters.location} 62 | 63 |

64 |
65 |
66 | {properties?.map((property) => 67 | viewMode === "grid" ? ( 68 | fav.id === property.id 74 | ) || false 75 | } 76 | onFavoriteToggle={() => handleFavoriteToggle(property.id)} 77 | showFavoriteButton={!!authUser} 78 | propertyLink={`/search/${property.id}`} 79 | /> 80 | ) : ( 81 | fav.id === property.id 87 | ) || false 88 | } 89 | onFavoriteToggle={() => handleFavoriteToggle(property.id)} 90 | showFavoriteButton={!!authUser} 91 | propertyLink={`/search/${property.id}`} 92 | /> 93 | ) 94 | )} 95 |
96 |
97 |
98 | ); 99 | }; 100 | 101 | export default Listings; 102 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/search/Map.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useEffect, useRef } from "react"; 3 | import mapboxgl from "mapbox-gl"; 4 | import "mapbox-gl/dist/mapbox-gl.css"; 5 | import { useAppSelector } from "@/state/redux"; 6 | import { useGetPropertiesQuery } from "@/state/api"; 7 | import { Property } from "@/types/prismaTypes"; 8 | 9 | mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN as string; 10 | 11 | const Map = () => { 12 | const mapContainerRef = useRef(null); 13 | const filters = useAppSelector((state) => state.global.filters); 14 | const { 15 | data: properties, 16 | isLoading, 17 | isError, 18 | } = useGetPropertiesQuery(filters); 19 | 20 | useEffect(() => { 21 | if (isLoading || isError || !properties) return; 22 | 23 | const map = new mapboxgl.Map({ 24 | container: mapContainerRef.current!, 25 | style: "mapbox://styles/majesticglue/cm6u301pq008b01sl7yk1cnvb", 26 | center: filters.coordinates || [-74.5, 40], 27 | zoom: 9, 28 | }); 29 | 30 | properties.forEach((property) => { 31 | const marker = createPropertyMarker(property, map); 32 | const markerElement = marker.getElement(); 33 | const path = markerElement.querySelector("path[fill='#3FB1CE']"); 34 | if (path) path.setAttribute("fill", "#000000"); 35 | }); 36 | 37 | const resizeMap = () => { 38 | if (map) setTimeout(() => map.resize(), 700); 39 | }; 40 | resizeMap(); 41 | 42 | return () => map.remove(); 43 | }, [isLoading, isError, properties, filters.coordinates]); 44 | 45 | if (isLoading) return <>Loading...; 46 | if (isError || !properties) return
Failed to fetch properties
; 47 | 48 | return ( 49 |
50 |
58 |
59 | ); 60 | }; 61 | 62 | const createPropertyMarker = (property: Property, map: mapboxgl.Map) => { 63 | const marker = new mapboxgl.Marker() 64 | .setLngLat([ 65 | property.location.coordinates.longitude, 66 | property.location.coordinates.latitude, 67 | ]) 68 | .setPopup( 69 | new mapboxgl.Popup().setHTML( 70 | ` 71 |
72 |
73 |
74 | ${property.name} 75 |

76 | $${property.pricePerMonth} 77 | / month 78 |

79 |
80 |
81 | ` 82 | ) 83 | ) 84 | .addTo(map); 85 | return marker; 86 | }; 87 | 88 | export default Map; 89 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/search/[id]/ApplicationModal.tsx: -------------------------------------------------------------------------------- 1 | import { CustomFormField } from "@/components/FormField"; 2 | import { Button } from "@/components/ui/button"; 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogHeader, 7 | DialogTitle, 8 | } from "@/components/ui/dialog"; 9 | import { Form } from "@/components/ui/form"; 10 | import { ApplicationFormData, applicationSchema } from "@/lib/schemas"; 11 | import { useCreateApplicationMutation, useGetAuthUserQuery } from "@/state/api"; 12 | import { zodResolver } from "@hookform/resolvers/zod"; 13 | import React from "react"; 14 | import { useForm } from "react-hook-form"; 15 | 16 | const ApplicationModal = ({ 17 | isOpen, 18 | onClose, 19 | propertyId, 20 | }: ApplicationModalProps) => { 21 | const [createApplication] = useCreateApplicationMutation(); 22 | const { data: authUser } = useGetAuthUserQuery(); 23 | 24 | const form = useForm({ 25 | resolver: zodResolver(applicationSchema), 26 | defaultValues: { 27 | name: "", 28 | email: "", 29 | phoneNumber: "", 30 | message: "", 31 | }, 32 | }); 33 | 34 | const onSubmit = async (data: ApplicationFormData) => { 35 | if (!authUser || authUser.userRole !== "tenant") { 36 | console.error( 37 | "You must be logged in as a tenant to submit an application" 38 | ); 39 | return; 40 | } 41 | 42 | await createApplication({ 43 | ...data, 44 | applicationDate: new Date().toISOString(), 45 | status: "Pending", 46 | propertyId: propertyId, 47 | tenantCognitoId: authUser.cognitoInfo.userId, 48 | }); 49 | onClose(); 50 | }; 51 | 52 | return ( 53 | 54 | 55 | 56 | Submit Application for this Property 57 | 58 |
59 | 60 | 66 | 72 | 78 | 84 | 87 | 88 | 89 |
90 |
91 | ); 92 | }; 93 | 94 | export default ApplicationModal; 95 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/search/[id]/ContactWidget.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { useGetAuthUserQuery } from "@/state/api"; 3 | import { Phone } from "lucide-react"; 4 | import { useRouter } from "next/navigation"; 5 | import React from "react"; 6 | 7 | const ContactWidget = ({ onOpenModal }: ContactWidgetProps) => { 8 | const { data: authUser } = useGetAuthUserQuery(); 9 | const router = useRouter(); 10 | 11 | const handleButtonClick = () => { 12 | if (authUser) { 13 | onOpenModal(); 14 | } else { 15 | router.push("/signin"); 16 | } 17 | }; 18 | 19 | return ( 20 |
21 | {/* Contact Property */} 22 |
23 |
24 | 25 |
26 |
27 |

Contact This Property

28 |
29 | (424) 340-5574 30 |
31 |
32 |
33 | 39 | 40 |
41 |
42 |
Language: English, Bahasa.
43 |
44 | Open by appointment on Monday - Sunday 45 |
46 |
47 |
48 | ); 49 | }; 50 | 51 | export default ContactWidget; 52 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/search/[id]/ImagePreviews.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ChevronLeft, ChevronRight } from "lucide-react"; 4 | import Image from "next/image"; 5 | import React, { useState } from "react"; 6 | 7 | const ImagePreviews = ({ images }: ImagePreviewsProps) => { 8 | const [currentImageIndex, setCurrentImageIndex] = useState(0); 9 | 10 | const handlePrev = () => { 11 | setCurrentImageIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1)); 12 | }; 13 | 14 | const handleNext = () => { 15 | setCurrentImageIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1)); 16 | }; 17 | 18 | return ( 19 |
20 | {images.map((image, index) => ( 21 |
27 | {`Property 34 |
35 | ))} 36 | 43 | 50 |
51 | ); 52 | }; 53 | 54 | export default ImagePreviews; 55 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/search/[id]/PropertyDetails.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 2 | import { AmenityIcons, HighlightIcons } from "@/lib/constants"; 3 | import { formatEnumString } from "@/lib/utils"; 4 | import { useGetPropertyQuery } from "@/state/api"; 5 | import { HelpCircle } from "lucide-react"; 6 | import React from "react"; 7 | 8 | const PropertyDetails = ({ propertyId }: PropertyDetailsProps) => { 9 | const { 10 | data: property, 11 | isError, 12 | isLoading, 13 | } = useGetPropertyQuery(propertyId); 14 | 15 | if (isLoading) return <>Loading...; 16 | if (isError || !property) { 17 | return <>Property not Found; 18 | } 19 | 20 | return ( 21 |
22 | {/* Amenities */} 23 |
24 |

Property Amenities

25 |
26 | {property.amenities.map((amenity: AmenityEnum) => { 27 | const Icon = AmenityIcons[amenity as AmenityEnum] || HelpCircle; 28 | return ( 29 |
33 | 34 | 35 | {formatEnumString(amenity)} 36 | 37 |
38 | ); 39 | })} 40 |
41 |
42 | 43 | {/* Highlights */} 44 |
45 |

46 | Highlights 47 |

48 |
49 | {property.highlights.map((highlight: HighlightEnum) => { 50 | const Icon = 51 | HighlightIcons[highlight as HighlightEnum] || HelpCircle; 52 | return ( 53 |
57 | 58 | 59 | {formatEnumString(highlight)} 60 | 61 |
62 | ); 63 | })} 64 |
65 |
66 | 67 | {/* Tabs Section */} 68 |
69 |

70 | Fees and Policies 71 |

72 |

73 | The fees below are based on community-supplied data and may exclude 74 | additional fees and utilities. 75 |

76 | 77 | 78 | Required Fees 79 | Pets 80 | Parking 81 | 82 | 83 |

One time move in fees

84 |
85 |
86 | 87 | Application Fee 88 | 89 | 90 | ${property.applicationFee} 91 | 92 |
93 |
94 |
95 | 96 | Security Deposit 97 | 98 | 99 | ${property.securityDeposit} 100 | 101 |
102 |
103 |
104 | 105 |

106 | Pets are {property.isPetsAllowed ? "allowed" : "not allowed"} 107 |

108 |
109 | 110 |

111 | Parking is{" "} 112 | {property.isParkingIncluded ? "included" : "not included"} 113 |

114 |
115 |
116 |
117 |
118 | ); 119 | }; 120 | 121 | export default PropertyDetails; 122 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/search/[id]/PropertyLocation.tsx: -------------------------------------------------------------------------------- 1 | import { useGetPropertyQuery } from "@/state/api"; 2 | import { Compass, MapPin } from "lucide-react"; 3 | import mapboxgl from "mapbox-gl"; 4 | import "mapbox-gl/dist/mapbox-gl.css"; 5 | import React, { useEffect, useRef } from "react"; 6 | 7 | mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN as string; 8 | 9 | const PropertyLocation = ({ propertyId }: PropertyDetailsProps) => { 10 | const { 11 | data: property, 12 | isError, 13 | isLoading, 14 | } = useGetPropertyQuery(propertyId); 15 | const mapContainerRef = useRef(null); 16 | 17 | useEffect(() => { 18 | if (isLoading || isError || !property) return; 19 | 20 | const map = new mapboxgl.Map({ 21 | container: mapContainerRef.current!, 22 | style: "mapbox://styles/majesticglue/cm6u301pq008b01sl7yk1cnvb", 23 | center: [ 24 | property.location.coordinates.longitude, 25 | property.location.coordinates.latitude, 26 | ], 27 | zoom: 14, 28 | }); 29 | 30 | const marker = new mapboxgl.Marker() 31 | .setLngLat([ 32 | property.location.coordinates.longitude, 33 | property.location.coordinates.latitude, 34 | ]) 35 | .addTo(map); 36 | 37 | const markerElement = marker.getElement(); 38 | const path = markerElement.querySelector("path[fill='#3FB1CE']"); 39 | if (path) path.setAttribute("fill", "#000000"); 40 | 41 | return () => map.remove(); 42 | }, [property, isError, isLoading]); 43 | 44 | if (isLoading) return <>Loading...; 45 | if (isError || !property) { 46 | return <>Property not Found; 47 | } 48 | 49 | return ( 50 |
51 |

52 | Map and Location 53 |

54 |
55 |
56 | 57 | Property Address: 58 | 59 | {property.location?.address || "Address not available"} 60 | 61 |
62 | 70 | 71 | Get Directions 72 | 73 |
74 |
78 |
79 | ); 80 | }; 81 | 82 | export default PropertyLocation; 83 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/search/[id]/PropertyOverview.tsx: -------------------------------------------------------------------------------- 1 | import { useGetPropertyQuery } from "@/state/api"; 2 | import { MapPin, Star } from "lucide-react"; 3 | import React from "react"; 4 | 5 | const PropertyOverview = ({ propertyId }: PropertyOverviewProps) => { 6 | const { 7 | data: property, 8 | isError, 9 | isLoading, 10 | } = useGetPropertyQuery(propertyId); 11 | 12 | if (isLoading) return <>Loading...; 13 | if (isError || !property) { 14 | return <>Property not Found; 15 | } 16 | 17 | return ( 18 |
19 | {/* Header */} 20 |
21 |
22 | {property.location?.country} / {property.location?.state} /{" "} 23 | 24 | {property.location?.city} 25 | 26 |
27 |

{property.name}

28 |
29 | 30 | 31 | {property.location?.city}, {property.location?.state},{" "} 32 | {property.location?.country} 33 | 34 |
35 | 36 | 37 | {property.averageRating.toFixed(1)} ({property.numberOfReviews}{" "} 38 | Reviews) 39 | 40 | Verified Listing 41 |
42 |
43 |
44 | 45 | {/* Details */} 46 |
47 |
48 |
49 |
Monthly Rent
50 |
51 | ${property.pricePerMonth.toLocaleString()} 52 |
53 |
54 |
55 |
56 |
Bedrooms
57 |
{property.beds} bd
58 |
59 |
60 |
61 |
Bathrooms
62 |
{property.baths} ba
63 |
64 |
65 |
66 |
Square Feet
67 |
68 | {property.squareFeet.toLocaleString()} sq ft 69 |
70 |
71 |
72 |
73 | 74 | {/* Summary */} 75 |
76 |

About {property.name}

77 |

78 | {property.description} 79 | Experience resort style luxury living at Seacrest Homes, where the 80 | ocean and city are seamlessly intertwined. Our newly built community 81 | features sophisticated two and three-bedroom residences, each complete 82 | with high end designer finishes, quartz counter tops, stainless steel 83 | whirlpool appliances, office nook, and a full size in-unit washer and 84 | dryer. Find your personal escape at home beside stunning swimming 85 | pools and spas with poolside cabanas. Experience your very own oasis 86 | surrounded by lavish landscaped courtyards, with indoor/outdoor 87 | entertainment seating. By day, lounge in the BBQ area and experience 88 | the breath taking unobstructed views stretching from the Palos Verdes 89 | Peninsula to Downtown Los Angeles, or watch the beauty of the South 90 | Bay skyline light up by night. Start or end your day with a workout in 91 | our full-size state of the art fitness club and yoga studio. Save the 92 | commute and plan your next meeting in the business centers conference 93 | room, adjacent to our internet and coffee lounge. Conveniently located 94 | near beautiful local beaches with easy access to the 110, 405 and 91 95 | freeways, exclusive shopping at the largest mall in the Western United 96 | States “The Del Amo Fashion Center” to the hospital of your choice, 97 | Kaiser Hospital, UCLA Harbor Medical Center, Torrance Memorial Medical 98 | Center, and Providence Little Company of Mary Hospital Torrance rated 99 | one of the top 10 Best in Los Angeles. Contact us today to tour and 100 | embrace the Seacrest luxury lifestyle as your own. Seacrest Homes 101 | Apartments is an apartment community located in Los Angeles County and 102 | the 90501 ZIP Code. This area is served by the Los Angeles Unified 103 | attendance zone. 104 |

105 |
106 |
107 | ); 108 | }; 109 | 110 | export default PropertyOverview; 111 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/search/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useGetAuthUserQuery } from "@/state/api"; 4 | import { useParams } from "next/navigation"; 5 | import React, { useState } from "react"; 6 | import ImagePreviews from "./ImagePreviews"; 7 | import PropertyOverview from "./PropertyOverview"; 8 | import PropertyDetails from "./PropertyDetails"; 9 | import PropertyLocation from "./PropertyLocation"; 10 | import ContactWidget from "./ContactWidget"; 11 | import ApplicationModal from "./ApplicationModal"; 12 | 13 | const SingleListing = () => { 14 | const { id } = useParams(); 15 | const propertyId = Number(id); 16 | const [isModalOpen, setIsModalOpen] = useState(false); 17 | const { data: authUser } = useGetAuthUserQuery(); 18 | 19 | return ( 20 |
21 | 24 |
25 |
26 | 27 | 28 | 29 |
30 | 31 |
32 | setIsModalOpen(true)} /> 33 |
34 |
35 | 36 | {authUser && ( 37 | setIsModalOpen(false)} 40 | propertyId={propertyId} 41 | /> 42 | )} 43 |
44 | ); 45 | }; 46 | 47 | export default SingleListing; 48 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/search/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { NAVBAR_HEIGHT } from "@/lib/constants"; 4 | import { useAppDispatch, useAppSelector } from "@/state/redux"; 5 | import { useSearchParams } from "next/navigation"; 6 | import React, { useEffect } from "react"; 7 | import FiltersBar from "./FiltersBar"; 8 | import FiltersFull from "./FiltersFull"; 9 | import { cleanParams } from "@/lib/utils"; 10 | import { setFilters } from "@/state"; 11 | import Map from "./Map"; 12 | import Listings from "./Listings"; 13 | 14 | const SearchPage = () => { 15 | const searchParams = useSearchParams(); 16 | const dispatch = useAppDispatch(); 17 | const isFiltersFullOpen = useAppSelector( 18 | (state) => state.global.isFiltersFullOpen 19 | ); 20 | 21 | useEffect(() => { 22 | const initialFilters = Array.from(searchParams.entries()).reduce( 23 | (acc: any, [key, value]) => { 24 | if (key === "priceRange" || key === "squareFeet") { 25 | acc[key] = value.split(",").map((v) => (v === "" ? null : Number(v))); 26 | } else if (key === "coordinates") { 27 | acc[key] = value.split(",").map(Number); 28 | } else { 29 | acc[key] = value === "any" ? null : value; 30 | } 31 | 32 | return acc; 33 | }, 34 | {} 35 | ); 36 | 37 | const cleanedFilters = cleanParams(initialFilters); 38 | dispatch(setFilters(cleanedFilters)); 39 | }, []); // eslint-disable-line react-hooks/exhaustive-deps 40 | 41 | return ( 42 |
48 | 49 |
50 |
57 | 58 |
59 | 60 |
61 | 62 |
63 |
64 |
65 | ); 66 | }; 67 | 68 | export default SearchPage; 69 | -------------------------------------------------------------------------------- /client/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/real-estate-prod/32828d842dc3b82c14cf45ffe1ea3d620c4c4a7b/client/src/app/favicon.ico -------------------------------------------------------------------------------- /client/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | import Providers from "./providers"; 5 | import { Toaster } from "@/components/ui/sonner"; 6 | 7 | const geistSans = Geist({ 8 | variable: "--font-geist-sans", 9 | subsets: ["latin"], 10 | }); 11 | 12 | const geistMono = Geist_Mono({ 13 | variable: "--font-geist-mono", 14 | subsets: ["latin"], 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: "Create Next App", 19 | description: "Generated by create next app", 20 | }; 21 | 22 | export default function RootLayout({ 23 | children, 24 | }: Readonly<{ 25 | children: React.ReactNode; 26 | }>) { 27 | return ( 28 | 29 | 32 | {children} 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /client/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from "@/components/Navbar"; 2 | import Landing from "./(nondashboard)/landing/page"; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 | 8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /client/src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import StoreProvider from "@/state/redux"; 4 | import { Authenticator } from "@aws-amplify/ui-react"; 5 | import Auth from "./(auth)/authProvider"; 6 | 7 | const Providers = ({ children }: { children: React.ReactNode }) => { 8 | return ( 9 | 10 | 11 | {children} 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default Providers; 18 | -------------------------------------------------------------------------------- /client/src/components/AppSidebar.tsx: -------------------------------------------------------------------------------- 1 | import { usePathname } from "next/navigation"; 2 | import React from "react"; 3 | import { 4 | Sidebar, 5 | SidebarContent, 6 | SidebarHeader, 7 | SidebarMenu, 8 | SidebarMenuButton, 9 | SidebarMenuItem, 10 | useSidebar, 11 | } from "./ui/sidebar"; 12 | import { 13 | Building, 14 | FileText, 15 | Heart, 16 | Home, 17 | Menu, 18 | Settings, 19 | X, 20 | } from "lucide-react"; 21 | import { NAVBAR_HEIGHT } from "@/lib/constants"; 22 | import { cn } from "@/lib/utils"; 23 | import Link from "next/link"; 24 | 25 | const AppSidebar = ({ userType }: AppSidebarProps) => { 26 | const pathname = usePathname(); 27 | const { toggleSidebar, open } = useSidebar(); 28 | 29 | const navLinks = 30 | userType === "manager" 31 | ? [ 32 | { icon: Building, label: "Properties", href: "/managers/properties" }, 33 | { 34 | icon: FileText, 35 | label: "Applications", 36 | href: "/managers/applications", 37 | }, 38 | { icon: Settings, label: "Settings", href: "/managers/settings" }, 39 | ] 40 | : [ 41 | { icon: Heart, label: "Favorites", href: "/tenants/favorites" }, 42 | { 43 | icon: FileText, 44 | label: "Applications", 45 | href: "/tenants/applications", 46 | }, 47 | { icon: Home, label: "Residences", href: "/tenants/residences" }, 48 | { icon: Settings, label: "Settings", href: "/tenants/settings" }, 49 | ]; 50 | 51 | return ( 52 | 60 | 61 | 62 | 63 |
69 | {open ? ( 70 | <> 71 |

72 | {userType === "manager" ? "Manager View" : "Renter View"} 73 |

74 | 80 | 81 | ) : ( 82 | 88 | )} 89 |
90 |
91 |
92 |
93 | 94 | 95 | 96 | {navLinks.map((link) => { 97 | const isActive = pathname === link.href; 98 | 99 | return ( 100 | 101 | 111 | 112 |
113 | 118 | 123 | {link.label} 124 | 125 |
126 | 127 |
128 |
129 | ); 130 | })} 131 |
132 |
133 |
134 | ); 135 | }; 136 | 137 | export default AppSidebar; 138 | -------------------------------------------------------------------------------- /client/src/components/ApplicationCard.tsx: -------------------------------------------------------------------------------- 1 | import { Mail, MapPin, PhoneCall } from "lucide-react"; 2 | import Image from "next/image"; 3 | import React, { useState } from "react"; 4 | 5 | const ApplicationCard = ({ 6 | application, 7 | userType, 8 | children, 9 | }: ApplicationCardProps) => { 10 | const [imgSrc, setImgSrc] = useState( 11 | application.property.photoUrls?.[0] || "/placeholder.jpg" 12 | ); 13 | 14 | const statusColor = 15 | application.status === "Approved" 16 | ? "bg-green-500" 17 | : application.status === "Denied" 18 | ? "bg-red-500" 19 | : "bg-yellow-500"; 20 | 21 | const contactPerson = 22 | userType === "manager" ? application.tenant : application.manager; 23 | 24 | return ( 25 |
26 |
27 | {/* Property Info Section */} 28 |
29 | {application.property.name} setImgSrc("/placeholder.jpg")} 37 | /> 38 |
39 |
40 |

41 | {application.property.name} 42 |

43 |
44 | 45 | {`${application.property.location.city}, ${application.property.location.country}`} 46 |
47 |
48 |
49 | ${application.property.pricePerMonth}{" "} 50 | / month 51 |
52 |
53 |
54 | 55 | {/* Divider - visible only on desktop */} 56 |
57 | 58 | {/* Status Section */} 59 |
60 |
61 |
62 | Status: 63 | 66 | {application.status} 67 | 68 |
69 |
70 |
71 |
72 | Start Date:{" "} 73 | {new Date(application.lease?.startDate).toLocaleDateString()} 74 |
75 |
76 | End Date:{" "} 77 | {new Date(application.lease?.endDate).toLocaleDateString()} 78 |
79 |
80 | Next Payment:{" "} 81 | {new Date(application.lease?.nextPaymentDate).toLocaleDateString()} 82 |
83 |
84 | 85 | {/* Divider - visible only on desktop */} 86 |
87 | 88 | {/* Contact Person Section */} 89 |
90 |
91 |
92 | {userType === "manager" ? "Tenant" : "Manager"} 93 |
94 |
95 |
96 |
97 |
98 | {contactPerson.name} 105 |
106 |
107 |
{contactPerson.name}
108 |
109 | 110 | {contactPerson.phoneNumber} 111 |
112 |
113 | 114 | {contactPerson.email} 115 |
116 |
117 |
118 |
119 |
120 | 121 |
122 | {children} 123 |
124 | ); 125 | }; 126 | 127 | export default ApplicationCard; 128 | -------------------------------------------------------------------------------- /client/src/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import { Bath, Bed, Heart, House, Star } from "lucide-react"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import React, { useState } from "react"; 5 | 6 | const Card = ({ 7 | property, 8 | isFavorite, 9 | onFavoriteToggle, 10 | showFavoriteButton = true, 11 | propertyLink, 12 | }: CardProps) => { 13 | const [imgSrc, setImgSrc] = useState( 14 | property.photoUrls?.[0] || "/placeholder.jpg" 15 | ); 16 | 17 | return ( 18 |
19 |
20 |
21 | {property.name} setImgSrc("/placeholder.jpg")} 28 | /> 29 |
30 |
31 | {property.isPetsAllowed && ( 32 | 33 | Pets Allowed 34 | 35 | )} 36 | {property.isParkingIncluded && ( 37 | 38 | Parking Included 39 | 40 | )} 41 |
42 | {showFavoriteButton && ( 43 | 53 | )} 54 |
55 |
56 |

57 | {propertyLink ? ( 58 | 63 | {property.name} 64 | 65 | ) : ( 66 | property.name 67 | )} 68 |

69 |

70 | {property?.location?.address}, {property?.location?.city} 71 |

72 |
73 |
74 | 75 | 76 | {property.averageRating.toFixed(1)} 77 | 78 | 79 | ({property.numberOfReviews} Reviews) 80 | 81 |
82 |

83 | ${property.pricePerMonth.toFixed(0)}{" "} 84 | /month 85 |

86 |
87 |
88 |
89 | 90 | 91 | {property.beds} Bed 92 | 93 | 94 | 95 | {property.baths} Bath 96 | 97 | 98 | 99 | {property.squareFeet} sq ft 100 | 101 |
102 |
103 |
104 | ); 105 | }; 106 | 107 | export default Card; 108 | -------------------------------------------------------------------------------- /client/src/components/CardCompact.tsx: -------------------------------------------------------------------------------- 1 | import { Bath, Bed, Heart, House, Star } from "lucide-react"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import React, { useState } from "react"; 5 | 6 | const CardCompact = ({ 7 | property, 8 | isFavorite, 9 | onFavoriteToggle, 10 | showFavoriteButton = true, 11 | propertyLink, 12 | }: CardCompactProps) => { 13 | const [imgSrc, setImgSrc] = useState( 14 | property.photoUrls?.[0] || "/placeholder.jpg" 15 | ); 16 | 17 | return ( 18 |
19 |
20 | {property.name} setImgSrc("/placeholder.jpg")} 27 | /> 28 |
29 | {property.isPetsAllowed && ( 30 | 31 | Pets 32 | 33 | )} 34 | {property.isParkingIncluded && ( 35 | 36 | Parking 37 | 38 | )} 39 |
40 |
41 |
42 |
43 |
44 |

45 | {propertyLink ? ( 46 | 51 | {property.name} 52 | 53 | ) : ( 54 | property.name 55 | )} 56 |

57 | {showFavoriteButton && ( 58 | 68 | )} 69 |
70 |

71 | {property?.location?.address}, {property?.location?.city} 72 |

73 |
74 | 75 | 76 | {property.averageRating.toFixed(1)} 77 | 78 | 79 | ({property.numberOfReviews}) 80 | 81 |
82 |
83 |
84 |
85 | 86 | 87 | {property.beds} 88 | 89 | 90 | 91 | {property.baths} 92 | 93 | 94 | 95 | {property.squareFeet} 96 | 97 |
98 | 99 |

100 | ${property.pricePerMonth.toFixed(0)} 101 | /mo 102 |

103 |
104 |
105 |
106 | ); 107 | }; 108 | 109 | export default CardCompact; 110 | -------------------------------------------------------------------------------- /client/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Header = ({ title, subtitle }: HeaderProps) => { 4 | return ( 5 |
6 |

{title}

7 |

{subtitle}

8 |
9 | ); 10 | }; 11 | 12 | export default Header; 13 | -------------------------------------------------------------------------------- /client/src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2 } from "lucide-react"; 2 | import React from "react"; 3 | 4 | const Loading = () => { 5 | return ( 6 |
7 | 8 | Loading... 9 |
10 | ); 11 | }; 12 | 13 | export default Loading; 14 | -------------------------------------------------------------------------------- /client/src/components/SettingsForm.tsx: -------------------------------------------------------------------------------- 1 | import { SettingsFormData, settingsSchema } from "@/lib/schemas"; 2 | import { zodResolver } from "@hookform/resolvers/zod"; 3 | import React, { useState } from "react"; 4 | import { useForm } from "react-hook-form"; 5 | import { Form } from "./ui/form"; 6 | import { CustomFormField } from "./FormField"; 7 | import { Button } from "./ui/button"; 8 | 9 | const SettingsForm = ({ 10 | initialData, 11 | onSubmit, 12 | userType, 13 | }: SettingsFormProps) => { 14 | const [editMode, setEditMode] = useState(false); 15 | const form = useForm({ 16 | resolver: zodResolver(settingsSchema), 17 | defaultValues: initialData, 18 | }); 19 | 20 | const toggleEditMode = () => { 21 | setEditMode(!editMode); 22 | if (editMode) { 23 | form.reset(initialData); 24 | } 25 | }; 26 | 27 | const handleSubmit = async (data: SettingsFormData) => { 28 | await onSubmit(data); 29 | setEditMode(false); 30 | }; 31 | 32 | return ( 33 |
34 |
35 |

36 | {`${userType.charAt(0).toUpperCase() + userType.slice(1)} Settings`} 37 |

38 |

39 | Manage your account preferences and personal information 40 |

41 |
42 |
43 |
44 | 48 | 49 | 55 | 60 | 61 |
62 | 69 | {editMode && ( 70 | 76 | )} 77 |
78 | 79 | 80 |
81 |
82 | ); 83 | }; 84 | 85 | export default SettingsForm; 86 | -------------------------------------------------------------------------------- /client/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /client/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /client/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /client/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /client/src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { Check } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )) 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 29 | 30 | export { Checkbox } 31 | -------------------------------------------------------------------------------- /client/src/components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { type DialogProps } from "@radix-ui/react-dialog" 5 | import { Command as CommandPrimitive } from "cmdk" 6 | import { Search } from "lucide-react" 7 | 8 | import { cn } from "@/lib/utils" 9 | import { Dialog, DialogContent } from "@/components/ui/dialog" 10 | 11 | const Command = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )) 24 | Command.displayName = CommandPrimitive.displayName 25 | 26 | const CommandDialog = ({ children, ...props }: DialogProps) => { 27 | return ( 28 | 29 | 30 | 31 | {children} 32 | 33 | 34 | 35 | ) 36 | } 37 | 38 | const CommandInput = React.forwardRef< 39 | React.ElementRef, 40 | React.ComponentPropsWithoutRef 41 | >(({ className, ...props }, ref) => ( 42 |
43 | 44 | 52 |
53 | )) 54 | 55 | CommandInput.displayName = CommandPrimitive.Input.displayName 56 | 57 | const CommandList = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, ...props }, ref) => ( 61 | 66 | )) 67 | 68 | CommandList.displayName = CommandPrimitive.List.displayName 69 | 70 | const CommandEmpty = React.forwardRef< 71 | React.ElementRef, 72 | React.ComponentPropsWithoutRef 73 | >((props, ref) => ( 74 | 79 | )) 80 | 81 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName 82 | 83 | const CommandGroup = React.forwardRef< 84 | React.ElementRef, 85 | React.ComponentPropsWithoutRef 86 | >(({ className, ...props }, ref) => ( 87 | 95 | )) 96 | 97 | CommandGroup.displayName = CommandPrimitive.Group.displayName 98 | 99 | const CommandSeparator = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName 110 | 111 | const CommandItem = React.forwardRef< 112 | React.ElementRef, 113 | React.ComponentPropsWithoutRef 114 | >(({ className, ...props }, ref) => ( 115 | 123 | )) 124 | 125 | CommandItem.displayName = CommandPrimitive.Item.displayName 126 | 127 | const CommandShortcut = ({ 128 | className, 129 | ...props 130 | }: React.HTMLAttributes) => { 131 | return ( 132 | 139 | ) 140 | } 141 | CommandShortcut.displayName = "CommandShortcut" 142 | 143 | export { 144 | Command, 145 | CommandDialog, 146 | CommandInput, 147 | CommandList, 148 | CommandEmpty, 149 | CommandGroup, 150 | CommandItem, 151 | CommandShortcut, 152 | CommandSeparator, 153 | } 154 | -------------------------------------------------------------------------------- /client/src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /client/src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { Slot } from "@radix-ui/react-slot" 6 | import { 7 | Controller, 8 | ControllerProps, 9 | FieldPath, 10 | FieldValues, 11 | FormProvider, 12 | useFormContext, 13 | } from "react-hook-form" 14 | 15 | import { cn } from "@/lib/utils" 16 | import { Label } from "@/components/ui/label" 17 | 18 | const Form = FormProvider 19 | 20 | type FormFieldContextValue< 21 | TFieldValues extends FieldValues = FieldValues, 22 | TName extends FieldPath = FieldPath 23 | > = { 24 | name: TName 25 | } 26 | 27 | const FormFieldContext = React.createContext( 28 | {} as FormFieldContextValue 29 | ) 30 | 31 | const FormField = < 32 | TFieldValues extends FieldValues = FieldValues, 33 | TName extends FieldPath = FieldPath 34 | >({ 35 | ...props 36 | }: ControllerProps) => { 37 | return ( 38 | 39 | 40 | 41 | ) 42 | } 43 | 44 | const useFormField = () => { 45 | const fieldContext = React.useContext(FormFieldContext) 46 | const itemContext = React.useContext(FormItemContext) 47 | const { getFieldState, formState } = useFormContext() 48 | 49 | const fieldState = getFieldState(fieldContext.name, formState) 50 | 51 | if (!fieldContext) { 52 | throw new Error("useFormField should be used within ") 53 | } 54 | 55 | const { id } = itemContext 56 | 57 | return { 58 | id, 59 | name: fieldContext.name, 60 | formItemId: `${id}-form-item`, 61 | formDescriptionId: `${id}-form-item-description`, 62 | formMessageId: `${id}-form-item-message`, 63 | ...fieldState, 64 | } 65 | } 66 | 67 | type FormItemContextValue = { 68 | id: string 69 | } 70 | 71 | const FormItemContext = React.createContext( 72 | {} as FormItemContextValue 73 | ) 74 | 75 | const FormItem = React.forwardRef< 76 | HTMLDivElement, 77 | React.HTMLAttributes 78 | >(({ className, ...props }, ref) => { 79 | const id = React.useId() 80 | 81 | return ( 82 | 83 |
84 | 85 | ) 86 | }) 87 | FormItem.displayName = "FormItem" 88 | 89 | const FormLabel = React.forwardRef< 90 | React.ElementRef, 91 | React.ComponentPropsWithoutRef 92 | >(({ className, ...props }, ref) => { 93 | const { error, formItemId } = useFormField() 94 | 95 | return ( 96 |