Skip to content

About

A website for searching properties

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

Β 

History

20 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

IDXExchange

A full-stack real estate listing platform with AI-assisted search filter assistant, in-memory page caching, searchable property grid, multi-column sorting, favorite property bookmarking, open house calendar with date range filtering, property detail pages with photo carousels, full-screen galleries, interactive Google Maps, and open house schedules.

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Browser (:3000)                         β”‚
β”‚                                                                 β”‚
β”‚   React Router (BrowserRouter) + ErrorBoundary                  β”‚
β”‚   β”œβ”€β”€ /           β†’ IntroductionPage (hero + features)          β”‚
β”‚   β”œβ”€β”€ /search     β†’ ListingsPage (chat assistant + filters/sort)β”‚
β”‚   β”‚                 β”œβ”€β”€ ChatAssistant (collapsible AI chat)     β”‚
β”‚   β”‚                 β”œβ”€β”€ PropertyFilters                         β”‚
β”‚   β”‚                 β”œβ”€β”€ SortControls (multi-column)             β”‚
β”‚   β”‚                 β”œβ”€β”€ Pagination (top & bottom)               β”‚
β”‚   β”‚                 └── PropertyCard[] (target="_blank")        β”‚
β”‚   β”‚                       β”œβ”€β”€ PropertyImageCarousel             β”‚
β”‚   β”‚                       β”œβ”€β”€ Open House Badge (green)          β”‚
β”‚   β”‚                       β”œβ”€β”€ Status Badge (Active/Pending)     β”‚
β”‚   β”‚                       └── Favorite Heart Button (β™‘ / β™₯)     β”‚
β”‚   β”‚                                                             β”‚
β”‚   β”œβ”€β”€ /chat-search β†’ ChatSearchPage (conversational AI search)  β”‚
β”‚   β”‚                 β”œβ”€β”€ ChatAssistant (open by default)         β”‚
β”‚   β”‚                 β”œβ”€β”€ Direct API execution on filter update   β”‚
β”‚   β”‚                 β”œβ”€β”€ Pagination (top & bottom)               β”‚
β”‚   β”‚                 └── PropertyCard[] (target="_blank")        β”‚
β”‚   β”‚                                                             β”‚
β”‚   β”œβ”€β”€ /favorites  β†’ FavoritesPage (saved listings view)         β”‚
β”‚   β”‚                 β”œβ”€β”€ Remove All Button (header)              β”‚
β”‚   β”‚                 β”œβ”€β”€ ChatAssistant & PropertyFilters         β”‚
β”‚   β”‚                 β”œβ”€β”€ SortControls (multi-column)             β”‚
β”‚   β”‚                 └── PropertyCard[] (instant list shift)     β”‚
β”‚   β”‚                                                             β”‚
β”‚   β”œβ”€β”€ /openhouses β†’ OpenHousesPage (calendar + filters + cards) β”‚
β”‚   β”‚                 β”œβ”€β”€ ChatAssistant (AI dates + filters)      β”‚
β”‚   β”‚                 β”œβ”€β”€ react-big-calendar (month view)         β”‚
β”‚   β”‚                 β”œβ”€β”€ Date Range Filter (start/end inputs)   β”‚
β”‚   β”‚                 β”œβ”€β”€ PropertyFilters & SortControls          β”‚
β”‚   β”‚                 β”œβ”€β”€ Pagination (top & bottom)               β”‚
β”‚   β”‚                 └── OpenHouseCard[] (date/time/status)      β”‚
β”‚   β”‚                                                             β”‚
β”‚   └── /property/:id β†’ PropertyDetailPage (opens in new tab)     β”‚
β”‚                         β”œβ”€β”€ Save / Saved Favorite Button        β”‚
β”‚                         β”œβ”€β”€ PropertyImageGallery (swipe/lightbox)β”‚
β”‚                         β”œβ”€β”€ Centered Stat Icons (Square Feet)   β”‚
β”‚                         β”œβ”€β”€ Listing Agent Info Card             β”‚
β”‚                         β”œβ”€β”€ PropertyDetails (dynamic grid)      β”‚
β”‚                         β”œβ”€β”€ PropertyMap (address directions)    β”‚
β”‚                         └── Open Houses (active/expired/upcom)  β”‚
β”‚                                                                 β”‚
β”‚   useFavorites Hook (localStorage persistence + cross-tab sync) β”‚
β”‚   ErrorBoundary (catches rendering crashes + recovery UI)       β”‚
β”‚                                                                 β”‚
β”‚   fetch('/api/properties?city=Portland&sortBy=price,date')      β”‚
β”‚   fetch('/api/properties/:id')                                  β”‚
β”‚   fetch('/api/properties/:id/openhouses')                       β”‚
β”‚   fetchFavoriteProperties({ ids, ...filters, sortCriteria })    β”‚
β”‚   fetchAllOpenHouses({ startDate, endDate, sortCriteria, ... }) β”‚
β”‚   sendChatMessage({ messages, currentFilters, pageContext })    β”‚
β”‚         β”‚                                                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β”‚  Vite dev proxy (vite.config.js)
          β”‚  /api/* β†’ http://localhost:5000
          β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Express Backend (:5000)                       β”‚
β”‚                                                                 β”‚
β”‚   server.js β†’ app.js                                            β”‚
β”‚     β”œβ”€β”€ cors (origin whitelist & Vercel domain validation)      β”‚
β”‚     β”œβ”€β”€ rateLimiter (apiLimiter 300/15m + chatLimiter 15/m)     β”‚
β”‚     β”œβ”€β”€ requestLogger (middleware + high-res ms + X-Response-Time)β”‚
β”‚     β”œβ”€β”€ /api/health       β†’ health.js      β†’ SELECT 1          β”‚
β”‚     β”œβ”€β”€ /api/chat         β†’ chat.js        β†’ OpenRouter API    β”‚
β”‚     β”œβ”€β”€ /api/properties   β†’ properties.js  β†’ SELECT ... FROM   β”‚
β”‚     β”‚        β”œβ”€β”€ /              (search + hasOpenHouse + sort)  β”‚
β”‚     β”‚        β”œβ”€β”€ /favorites     (POST: ID list query + sort)    β”‚
β”‚     β”‚        β”œβ”€β”€ /:id           (driven by PROPERTY_DETAIL_COLS)β”‚
β”‚     β”‚        └── /:id/openhouses (JOIN + status logic)          β”‚
β”‚     └── /api/openhouses   β†’ openhouses.js  β†’ INNER JOIN        β”‚
β”‚                  └── /         (date range + filters + sort)    β”‚
β”‚                        β”‚                                        β”‚
β”‚                   db.js (connection pool)                        β”‚
β”‚                        β”‚                                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β”‚  mysql2/promise (port 3306)
                         β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  MySQL 8 Docker Container                        β”‚
β”‚                  (idx-mysql-local)                               β”‚
β”‚                                                                 β”‚
β”‚   Database: rets                                                β”‚
β”‚     β”œβ”€β”€ rets_property  (53,122 rows β€” listings, photos, coords) β”‚
β”‚     └── rets_openhouse (4,282 rows β€” open house events)         β”‚
β”‚                                                                 β”‚
β”‚   Imported from database/ SQL files on first container start    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Project Structure

IDXExchange/
β”œβ”€β”€ backend/                  # Node.js + Express REST API
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ server.js         # Entry point β€” starts Express
β”‚   β”‚   β”œβ”€β”€ app.js            # Express app (middleware + routes)
β”‚   β”‚   β”œβ”€β”€ config/db.js      # MySQL connection pool
β”‚   β”‚   β”œβ”€β”€ utils/logger.js   # Zero-dependency logger (URL redaction + error sanitization)
β”‚   β”‚   β”œβ”€β”€ middleware/       # requestLogger & rateLimiter
β”‚   β”‚   └── routes/           # health.js, properties.js, openhouses.js, chat.js
β”‚   β”œβ”€β”€ tests/                # Jest + Supertest (101 tests across 5 test suites)
β”‚   β”œβ”€β”€ .env                  # Backend env vars (LLM_API_KEY, DB vars, ALLOWED_ORIGINS)
β”‚   └── package.json
β”‚
β”œβ”€β”€ frontend/                 # React + Vite
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ main.jsx          # React entry point β€” wraps App in StrictMode
β”‚   β”‚   β”œβ”€β”€ App.jsx           # React Router setup + ErrorBoundary
β”‚   β”‚   β”œβ”€β”€ api/              # API client (propertyApi, chatApi)
β”‚   β”‚   β”œβ”€β”€ hooks/            # useFavorites hook (localStorage + cross-tab sync)
β”‚   β”‚   β”œβ”€β”€ utils/            # parsePhotos, formatPrice, formatTime, formatDate, prefetchCache
β”‚   β”‚   β”œβ”€β”€ test/             # Vitest setup + tests (84 tests across 13 suites)
β”‚   β”‚   β”œβ”€β”€ stylesheets/      # All CSS stylesheets
β”‚   β”‚   β”œβ”€β”€ components/       # UI components (Sidebar, PropertyCard, PropertyFilters, ChatAssistant, ErrorBoundary, etc.)
β”‚   β”‚   └── pages/            # IntroductionPage, ListingsPage, ChatSearchPage, FavoritesPage, OpenHousesPage, PropertyDetailPage
β”‚   β”œβ”€β”€ .env                  # VITE_GOOGLE_MAPS_API_KEY (gitignored)
β”‚   β”œβ”€β”€ .env.example          # Template for frontend env vars
β”‚   β”œβ”€β”€ vercel.json           # Vercel deployment config (API proxy rewrites + SPA fallback)
β”‚   β”œβ”€β”€ vite.config.js        # Dev server + API proxy + Vitest config
β”‚   └── package.json
β”‚
β”œβ”€β”€ database/                 # SQL imports (mounted into Docker)
β”‚   β”œβ”€β”€ 01_rets_openhouse.sql
β”‚   β”œβ”€β”€ 02_rets_property.sql
β”‚   └── 03_add_indexes.sql
β”‚
β”œβ”€β”€ docs/                     # Project documentation
β”‚   β”œβ”€β”€ ARCHITECTURE.md
β”‚   β”œβ”€β”€ CODING_STANDARDS.md
β”‚   β”œβ”€β”€ OVERVIEW.md
β”‚   β”œβ”€β”€ TASKS.md
β”‚   β”œβ”€β”€ SUPPORT_TASKS.md
β”‚   β”œβ”€β”€ LOCAL_RUN_GUIDE.md   # Dev vs Production local workflow & verification guide
β”‚   β”œβ”€β”€ CLOUD_DEPLOYMENT_GUIDE.md # Railway + Render + Vercel deployment guide
β”‚   β”œβ”€β”€ FILE_GUIDE.md        # Comprehensive file-by-file reference guide
β”‚   β”œβ”€β”€ TEST_GUIDE.md        # Comprehensive 185-test breakdown & diagnostic guide
β”‚   β”œβ”€β”€ change_log.md
β”‚   └── decision_log.md
β”‚
β”œβ”€β”€ docker-compose.yml        # MySQL 8 container config
β”œβ”€β”€ .env                      # Docker env vars (gitignored)
└── .gitignore

How the Pieces Connect

1. Database (MySQL in Docker)

Docker Compose starts a MySQL 8 container (idx-mysql-local) on port 3306. On first start, it automatically imports the SQL files from the database/ directory via Docker's /docker-entrypoint-initdb.d/ mechanism. This creates two tables:

  • rets_property β€” 53,122 property listings with address, price, beds, baths, photos (JSON), coordinates, StandardStatus, etc.
  • rets_openhouse β€” 4,282 open house events linked to properties by L_DisplayId

2. Backend (Express API)

The Express server connects to MySQL using a connection pool (db.js). It exposes REST endpoints under /api/:

  • GET /api/health β€” Runs SELECT 1 to verify the database is reachable
  • GET /api/properties β€” Queries rets_property with filters, multi-column sorting, pagination, and a hasOpenHouse boolean flag
  • POST /api/properties/favorites β€” Accepts an array of property display IDs in JSON body ({ ids: [...] }) and returns matching properties with filter, sort, and pagination support
  • GET /api/properties/:id β€” Fetches property details dynamically using a configurable PROPERTY_DETAIL_COLUMNS array
  • GET /api/properties/:id/openhouses β€” Fetches open house events from rets_openhouse joined with rets_property, adding server-side status logic (active, expired, upcoming)
  • GET /api/openhouses β€” Lists all open houses with optional startDate/endDate filtering, INNER JOIN with rets_property for property context, pagination (up to 500), and status computation

Every request is protected by origin-whitelisted CORS validation and tiered rate limiting (apiLimiter & chatLimiter), and logged by the requestLogger middleware with timestamp, method, URL, status code, duration in milliseconds, and attaches an X-Response-Time header.

3. Frontend (React + Vite)

The React app runs on port 3000 with a split-screen dashboard layout: a fixed sidebar on the left and a main content canvas on the right protected by a React Error Boundary (ErrorBoundary.jsx).

Navigation uses react-router-dom to route between pages:

  • / β€” Introduction Page (hero section with headline, CTA, and feature cards)
  • /search β€” Search Page (property filters + multi-column sort + listings grid + pagination + photo carousels + favorite heart buttons)
  • /favorites β€” Favorites Page (view and manage favorited listings, with Remove All button, filters, sort, pagination, and instant card removal)
  • /openhouses β€” Open Houses Page (react-big-calendar with range selection, date range filter panel, open house cards with property details, pagination)
  • /property/:id β€” Property Detail Page (Save/Saved favorite button, photo gallery with touch swipe + lightbox, centered stat icons with Square Feet label, Listing Agent Information card above Description, dynamic details grid, Google Maps embed with same-line Get Directions address link, and open house schedule)

Favorites state is managed globally by the custom useFavorites hook, which persists to localStorage and syncs across browser tabs in real time.

Clicking any property card on the Search or Favorites page opens the detail page in a new browser tab (target="_blank"), allowing users to keep browsing listings while examining property details.

Quick Start

Prerequisites

1. Start the database

# From project root
docker compose up -d

# Wait for the import to complete (rets_property is ~632 MB, takes several minutes)
docker compose logs -f mysql
# Look for: "ready for connections"

2. Start the backend

cd backend
cp .env.example .env   # First time only β€” edit if needed
npm install
npm run dev            # Starts on http://localhost:5000

3. Start the frontend

cd frontend
cp .env.example .env   # First time only β€” add your VITE_GOOGLE_MAPS_API_KEY
npm install
npm run dev            # Starts on http://localhost:3000

Open http://localhost:3000 to see the property listings.

4. Run tests & performance benchmarks

# Backend tests (101 tests across 5 suites)
cd backend
npm test

# Query Performance & EXPLAIN Benchmark Suite
npm run perf

# Frontend tests (84 Vitest tests across 13 suites)
cd frontend
npm test

Environment Variables

Root .env (Docker Compose)

Variable Description
MYSQL_ROOT_PASSWORD MySQL root password
MYSQL_DATABASE Database name (default: rets)
MYSQL_PORT Host port for MySQL (default: 3306)

backend/.env (Express)

Variable Description
DB_HOST / MYSQLHOST MySQL host (default: localhost for local dev)
DB_USER / MYSQLUSER MySQL user
DB_PASSWORD / MYSQLPASSWORD MySQL password
DB_NAME / MYSQLDATABASE Database name
DB_PORT / MYSQLPORT MySQL port (default: 3306)
MYSQL_URL / MYSQL_PUBLIC_URL Optional full connection URL string (Railway cloud / TCP proxy)
PORT Express server port (default: 5000)
ALLOWED_ORIGINS Comma-separated CORS allowed origins (production/staging)

frontend/.env (Vite)

Variable Description
VITE_GOOGLE_MAPS_API_KEY Google Maps API key (Maps Embed API enabled)

About

A website for searching properties

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages