Introduction to the Project
The Real Estate Listing Website is a comprehensive, full-stack web application designed to connect property buyers, sellers, and agents in a seamless digital marketplace. This platform provides a robust solution for listing, searching, and managing real estate properties with advanced features like property filtering, virtual tours, mortgage calculators, and user dashboards.
The system features role-based access control with four distinct user types: Admin, Agents, Buyers, and Sellers. Whether you're building a platform for a real estate agency, property management company, or independent agents, this system provides all the essential tools for modern real estate transactions.
Key Features
Admin Features
- Dashboard Overview: Comprehensive statistics on properties, users, and transactions
- User Management: Manage agents, buyers, and sellers with verification
- Property Moderation: Approve, reject, or feature property listings
- Category Management: Create and manage property types, amenities, and features
- Subscription Plans: Manage agent subscription packages and payments
- Analytics & Reports: Generate detailed reports on platform performance
- System Settings: Configure site settings, email templates, and payment gateways
- Advertisement Management: Manage banner ads and featured listings
Agent Features
- Agent Dashboard: View listing statistics, inquiries, and performance metrics
- Property Management: Add, edit, and manage property listings
- Media Management: Upload property photos, videos, and virtual tours
- Inquiry Management: Respond to buyer inquiries and schedule viewings
- Lead Management: Track potential buyers and manage client relationships
- Subscription Management: Upgrade plans and view payment history
- Performance Analytics: Track views, saves, and inquiries per property
- Featured Listings: Promote properties for better visibility
Buyer Features
- Property Search: Advanced search with filters (location, price, type, amenities)
- Saved Searches: Save search criteria and get alerts for new listings
- Favorite Properties: Save favorite properties for later viewing
- Property Comparison: Compare multiple properties side-by-side
- Schedule Viewings: Book property visits with agents
- Mortgage Calculator: Calculate monthly payments and affordability
- Neighborhood Info: View area information, schools, and amenities
- Saved Properties: Create wishlists and share with family
Seller Features
- Property Listings: List properties for sale with detailed information
- Agent Finder: Search and connect with real estate agents
- Valuation Tool: Get estimated property value
- Listing Management: Track listing performance and inquiries
- Document Upload: Upload property documents and certificates
- Offer Management: Review and respond to buyer offers
General Features
- Advanced Search: Filter by location, price range, property type, bedrooms, etc.
- Map Integration: Google Maps integration for property locations
- Virtual Tours: 360-degree virtual property tours
- Mortgage Calculator: Calculate monthly payments
- Email Alerts: Get notified about new matching properties
- Social Sharing: Share properties on social media
- Printable Brochures: Generate PDF brochures for properties
- Multi-language Support: Interface in multiple languages
- Responsive Design: Mobile-friendly interface
Technology Stack
- Frontend: HTML5, CSS3, JavaScript (ES6+), Bootstrap 5
- Backend: PHP 8.0+ (Core PHP with MVC-like structure)
- Database: MySQL 8.0+
- Additional Libraries:
- Google Maps API for location services
- Chart.js for analytics
- Bootstrap 5 for responsive UI
- Font Awesome for icons
- jQuery for AJAX operations
- Select2 for enhanced dropdowns
- DataTables for advanced tables
- Moment.js for date handling
- PHPMailer for email notifications
- TCPDF for PDF generation
- Intervention Image for image manipulation
- Stripe/PayPal SDK for payments
Project File Structure
real-estate/ │ ├── assets/ │ ├── css/ │ │ ├── style.css │ │ ├── dashboard.css │ │ ├── property.css │ │ ├── responsive.css │ │ └── dark-mode.css │ ├── js/ │ │ ├── main.js │ │ ├── search.js │ │ ├── property.js │ │ ├── map.js │ │ ├── mortgage.js │ │ ├── validation.js │ │ └── charts.js │ ├── images/ │ │ ├── properties/ │ │ ├── agents/ │ │ ├── avatars/ │ │ └── icons/ │ └── plugins/ │ ├── select2/ │ ├── datatables/ │ └── leaflet/ │ ├── includes/ │ ├── config.php │ ├── Database.php │ ├── functions.php │ ├── auth.php │ ├── Property.php │ ├── User.php │ ├── Agent.php │ ├── Inquiry.php │ ├── Favorite.php │ ├── Payment.php │ ├── Subscription.php │ ├── Review.php │ └── helpers/ │ ├── ImageHelper.php │ ├── MapHelper.php │ └── SearchHelper.php │ ├── admin/ │ ├── dashboard.php │ ├── manage_users.php │ ├── manage_agents.php │ ├── manage_properties.php │ ├── moderate_listings.php │ ├── manage_categories.php │ ├── manage_amenities.php │ ├── subscriptions.php │ ├── payments.php │ ├── reports.php │ ├── settings.php │ └── advertisements.php │ ├── agent/ │ ├── dashboard.php │ ├── properties.php │ ├── add_property.php │ ├── edit_property.php │ ├── media.php │ ├── inquiries.php │ ├── leads.php │ ├── schedule.php │ ├── subscription.php │ ├── payments.php │ ├── analytics.php │ ├── profile.php │ └── settings.php │ ├── buyer/ │ ├── dashboard.php │ ├── search.php │ ├── property.php │ ├── favorites.php │ ├── saved_searches.php │ ├── comparisons.php │ ├── inquiries.php │ ├── viewings.php │ ├── mortgage.php │ ├── profile.php │ └── settings.php │ ├── seller/ │ ├── dashboard.php │ ├── properties.php │ ├── add_property.php │ ├── agent_finder.php │ ├── valuation.php │ ├── documents.php │ ├── offers.php │ ├── profile.php │ └── settings.php │ ├── api/ │ ├── search.php │ ├── property.php │ ├── location.php │ ├── contact.php │ ├── favorite.php │ ├── compare.php │ └── valuation.php │ ├── uploads/ │ ├── properties/ │ ├── agents/ │ ├── users/ │ └── documents/ │ ├── vendor/ │ ├── index.php ├── login.php ├── register.php ├── forgot_password.php ├── reset_password.php ├── logout.php ├── properties.php ├── property-details.php ├── agents.php ├── agent-details.php ├── contact.php ├── about.php ├── .env ├── .gitignore ├── composer.json └── sql/ └── database.sql
Database Schema
File: sql/database.sql
-- Create Database
CREATE DATABASE IF NOT EXISTS `real_estate`;
USE `real_estate`;
-- Users Table
CREATE TABLE `users` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` VARCHAR(20) UNIQUE NOT NULL,
`email` VARCHAR(100) UNIQUE NOT NULL,
`password` VARCHAR(255) NOT NULL,
`first_name` VARCHAR(50) NOT NULL,
`last_name` VARCHAR(50) NOT NULL,
`phone` VARCHAR(20),
`phone_secondary` VARCHAR(20),
`date_of_birth` DATE,
`profile_picture` VARCHAR(255) DEFAULT 'default.jpg',
`role` ENUM('admin', 'agent', 'buyer', 'seller') NOT NULL DEFAULT 'buyer',
`status` ENUM('active', 'inactive', 'suspended', 'pending') DEFAULT 'pending',
`email_verified` BOOLEAN DEFAULT FALSE,
`phone_verified` BOOLEAN DEFAULT FALSE,
`verification_token` VARCHAR(255),
`reset_token` VARCHAR(255),
`reset_expires` DATETIME,
`last_login` DATETIME,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_email` (`email`),
INDEX `idx_role` (`role`),
INDEX `idx_status` (`status`)
);
-- Agents Table (extends users)
CREATE TABLE `agents` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`license_number` VARCHAR(50),
`agency_name` VARCHAR(255),
`agency_website` VARCHAR(255),
`years_experience` INT,
`specializations` TEXT,
`service_areas` TEXT,
`bio` TEXT,
`languages` TEXT,
`rating` DECIMAL(3,2) DEFAULT 0.00,
`total_reviews` INT DEFAULT 0,
`total_properties` INT DEFAULT 0,
`subscription_plan` ENUM('basic', 'premium', 'enterprise') DEFAULT 'basic',
`subscription_expires` DATE,
`featured` BOOLEAN DEFAULT FALSE,
`verified` BOOLEAN DEFAULT FALSE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
INDEX `idx_rating` (`rating`),
INDEX `idx_featured` (`featured`)
);
-- Property Types/Categories
CREATE TABLE `property_types` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`slug` VARCHAR(100) UNIQUE NOT NULL,
`description` TEXT,
`icon` VARCHAR(50) DEFAULT 'fa-building',
`parent_id` INT(11),
`sort_order` INT DEFAULT 0,
`is_active` BOOLEAN DEFAULT TRUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`parent_id`) REFERENCES `property_types`(`id`)
);
-- Property Status
CREATE TABLE `property_status` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL,
`slug` VARCHAR(50) UNIQUE NOT NULL,
`color` VARCHAR(7) DEFAULT '#6c757d',
`is_active` BOOLEAN DEFAULT TRUE,
`sort_order` INT DEFAULT 0,
PRIMARY KEY (`id`)
);
INSERT INTO `property_status` (`name`, `slug`, `color`) VALUES
('For Sale', 'for-sale', '#28a745'),
('For Rent', 'for-rent', '#17a2b8'),
('Sold', 'sold', '#dc3545'),
('Pending', 'pending', '#ffc107'),
('Under Contract', 'under-contract', '#fd7e14');
-- Amenities/Features
CREATE TABLE `amenities` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`icon` VARCHAR(50) DEFAULT 'fa-check',
`category` ENUM('interior', 'exterior', 'community', 'utilities') DEFAULT 'interior',
`is_active` BOOLEAN DEFAULT TRUE,
`sort_order` INT DEFAULT 0,
PRIMARY KEY (`id`)
);
-- Properties Table
CREATE TABLE `properties` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`property_id` VARCHAR(20) UNIQUE NOT NULL,
`agent_id` INT(11),
`seller_id` INT(11),
`title` VARCHAR(255) NOT NULL,
`slug` VARCHAR(255) UNIQUE NOT NULL,
`description` TEXT,
`property_type_id` INT(11) NOT NULL,
`status_id` INT(11) NOT NULL,
`price` DECIMAL(15,2) NOT NULL,
`price_sqft` DECIMAL(10,2),
`bedrooms` INT,
`bathrooms` DECIMAL(3,1),
`half_bathrooms` INT,
`sqft` INT,
`lot_size` DECIMAL(10,2),
`lot_size_unit` ENUM('sqft', 'acres', 'hectares') DEFAULT 'sqft',
`year_built` INT(4),
`floors` INT,
`parking_spaces` INT,
`parking_type` VARCHAR(50),
`address` VARCHAR(255) NOT NULL,
`city` VARCHAR(100) NOT NULL,
`state` VARCHAR(50),
`zipcode` VARCHAR(20),
`country` VARCHAR(50) DEFAULT 'USA',
`latitude` DECIMAL(10,8),
`longitude` DECIMAL(11,8),
`neighborhood` VARCHAR(100),
`school_district` VARCHAR(255),
`views` INT DEFAULT 0,
`featured` BOOLEAN DEFAULT FALSE,
`is_virtual_tour` BOOLEAN DEFAULT FALSE,
`virtual_tour_url` VARCHAR(255),
`video_url` VARCHAR(255),
`is_active` BOOLEAN DEFAULT TRUE,
`is_approved` BOOLEAN DEFAULT FALSE,
`approval_date` DATETIME,
`expiry_date` DATE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`agent_id`) REFERENCES `agents`(`id`) ON DELETE SET NULL,
FOREIGN KEY (`seller_id`) REFERENCES `users`(`id`) ON DELETE SET NULL,
FOREIGN KEY (`property_type_id`) REFERENCES `property_types`(`id`),
FOREIGN KEY (`status_id`) REFERENCES `property_status`(`id`),
INDEX `idx_price` (`price`),
INDEX `idx_city` (`city`),
INDEX `idx_featured` (`featured`),
INDEX `idx_active` (`is_active`),
FULLTEXT INDEX `idx_search` (`title`, `description`, `address`, `city`)
);
-- Property Amenities Junction
CREATE TABLE `property_amenities` (
`property_id` INT(11) NOT NULL,
`amenity_id` INT(11) NOT NULL,
PRIMARY KEY (`property_id`, `amenity_id`),
FOREIGN KEY (`property_id`) REFERENCES `properties`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`amenity_id`) REFERENCES `amenities`(`id`) ON DELETE CASCADE
);
-- Property Images
CREATE TABLE `property_images` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`property_id` INT(11) NOT NULL,
`image_path` VARCHAR(255) NOT NULL,
`is_primary` BOOLEAN DEFAULT FALSE,
`sort_order` INT DEFAULT 0,
`caption` VARCHAR(255),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`property_id`) REFERENCES `properties`(`id`) ON DELETE CASCADE,
INDEX `idx_primary` (`is_primary`)
);
-- Property Documents (for sellers)
CREATE TABLE `property_documents` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`property_id` INT(11) NOT NULL,
`document_path` VARCHAR(255) NOT NULL,
`document_name` VARCHAR(255),
`document_type` ENUM('deed', 'survey', 'inspection', 'appraisal', 'other') DEFAULT 'other',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`property_id`) REFERENCES `properties`(`id`) ON DELETE CASCADE
);
-- Property Views Tracking
CREATE TABLE `property_views` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`property_id` INT(11) NOT NULL,
`user_id` INT(11),
`ip_address` VARCHAR(45),
`user_agent` TEXT,
`viewed_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`property_id`) REFERENCES `properties`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL
);
-- Favorites / Saved Properties
CREATE TABLE `favorites` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`property_id` INT(11) NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_favorite` (`user_id`, `property_id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`property_id`) REFERENCES `properties`(`id`) ON DELETE CASCADE
);
-- Saved Searches
CREATE TABLE `saved_searches` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`name` VARCHAR(100) NOT NULL,
`search_params` JSON NOT NULL,
`email_alerts` BOOLEAN DEFAULT TRUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
-- Inquiries / Contact Requests
CREATE TABLE `inquiries` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`property_id` INT(11),
`agent_id` INT(11),
`user_id` INT(11),
`name` VARCHAR(100) NOT NULL,
`email` VARCHAR(100) NOT NULL,
`phone` VARCHAR(20),
`message` TEXT NOT NULL,
`type` ENUM('general', 'viewing', 'offer', 'question') DEFAULT 'general',
`status` ENUM('new', 'read', 'replied', 'archived') DEFAULT 'new',
`preferred_date` DATE,
`preferred_time` TIME,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`property_id`) REFERENCES `properties`(`id`) ON DELETE SET NULL,
FOREIGN KEY (`agent_id`) REFERENCES `agents`(`id`) ON DELETE SET NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL,
INDEX `idx_status` (`status`)
);
-- Viewing Appointments
CREATE TABLE `viewings` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`property_id` INT(11) NOT NULL,
`user_id` INT(11) NOT NULL,
`agent_id` INT(11) NOT NULL,
`scheduled_date` DATE NOT NULL,
`scheduled_time` TIME NOT NULL,
`duration` INT DEFAULT 30, -- minutes
`status` ENUM('pending', 'confirmed', 'completed', 'cancelled') DEFAULT 'pending',
`notes` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`property_id`) REFERENCES `properties`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`agent_id`) REFERENCES `agents`(`id`) ON DELETE CASCADE
);
-- Reviews and Ratings
CREATE TABLE `reviews` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`agent_id` INT(11) NOT NULL,
`user_id` INT(11) NOT NULL,
`rating` TINYINT NOT NULL CHECK (rating >= 1 AND rating <= 5),
`title` VARCHAR(255),
`comment` TEXT,
`response` TEXT,
`response_date` DATETIME,
`is_verified` BOOLEAN DEFAULT FALSE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`agent_id`) REFERENCES `agents`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
UNIQUE KEY `unique_review` (`agent_id`, `user_id`)
);
-- Subscription Plans
CREATE TABLE `subscription_plans` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL,
`slug` VARCHAR(50) UNIQUE NOT NULL,
`price` DECIMAL(10,2) NOT NULL,
`billing_cycle` ENUM('monthly', 'yearly') DEFAULT 'monthly',
`max_properties` INT,
`featured_listings` INT DEFAULT 0,
`virtual_tours` BOOLEAN DEFAULT FALSE,
`video_uploads` BOOLEAN DEFAULT FALSE,
`analytics` BOOLEAN DEFAULT FALSE,
`priority_support` BOOLEAN DEFAULT FALSE,
`description` TEXT,
`features` JSON,
`is_active` BOOLEAN DEFAULT TRUE,
`sort_order` INT DEFAULT 0,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
-- Agent Subscriptions
CREATE TABLE `agent_subscriptions` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`agent_id` INT(11) NOT NULL,
`plan_id` INT(11) NOT NULL,
`start_date` DATE NOT NULL,
`end_date` DATE NOT NULL,
`status` ENUM('active', 'expired', 'cancelled') DEFAULT 'active',
`payment_method` VARCHAR(50),
`transaction_id` VARCHAR(255),
`amount` DECIMAL(10,2),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`agent_id`) REFERENCES `agents`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`plan_id`) REFERENCES `subscription_plans`(`id`)
);
-- Payments Table
CREATE TABLE `payments` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`subscription_id` INT(11),
`amount` DECIMAL(10,2) NOT NULL,
`currency` VARCHAR(3) DEFAULT 'USD',
`payment_method` VARCHAR(50),
`transaction_id` VARCHAR(255),
`status` ENUM('pending', 'completed', 'failed', 'refunded') DEFAULT 'pending',
`payment_details` JSON,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
FOREIGN KEY (`subscription_id`) REFERENCES `agent_subscriptions`(`id`)
);
-- Notifications Table
CREATE TABLE `notifications` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`type` VARCHAR(50),
`title` VARCHAR(255) NOT NULL,
`message` TEXT NOT NULL,
`data` JSON,
`is_read` BOOLEAN DEFAULT FALSE,
`read_at` DATETIME,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
INDEX `idx_user_read` (`user_id`, `is_read`)
);
-- Blog/Articles (for content marketing)
CREATE TABLE `articles` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`slug` VARCHAR(255) UNIQUE NOT NULL,
`content` LONGTEXT,
`excerpt` TEXT,
`featured_image` VARCHAR(255),
`author_id` INT(11),
`category` VARCHAR(100),
`tags` TEXT,
`views` INT DEFAULT 0,
`is_published` BOOLEAN DEFAULT FALSE,
`published_at` DATETIME,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON DELETE SET NULL,
FULLTEXT INDEX `idx_search` (`title`, `content`)
);
-- FAQ Categories
CREATE TABLE `faq_categories` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`slug` VARCHAR(100) UNIQUE NOT NULL,
`sort_order` INT DEFAULT 0,
`is_active` BOOLEAN DEFAULT TRUE,
PRIMARY KEY (`id`)
);
-- FAQs
CREATE TABLE `faqs` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`category_id` INT(11),
`question` TEXT NOT NULL,
`answer` TEXT NOT NULL,
`sort_order` INT DEFAULT 0,
`is_active` BOOLEAN DEFAULT TRUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`category_id`) REFERENCES `faq_categories`(`id`) ON DELETE SET NULL
);
-- Site Settings
CREATE TABLE `settings` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`setting_key` VARCHAR(100) UNIQUE NOT NULL,
`setting_value` TEXT,
`description` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
-- Insert Default Admin
INSERT INTO `users` (`user_id`, `email`, `password`, `first_name`, `last_name`, `role`, `email_verified`, `status`)
VALUES ('ADMIN001', '[email protected]', '$2y$10$YourHashedPasswordHere', 'System', 'Administrator', 'admin', TRUE, 'active');
-- Insert Default Subscription Plans
INSERT INTO `subscription_plans` (`name`, `slug`, `price`, `billing_cycle`, `max_properties`, `featured_listings`, `features`) VALUES
('Basic', 'basic', 29.99, 'monthly', 10, 1, '["Basic listing", "Photo uploads", "Inquiry management"]'),
('Premium', 'premium', 49.99, 'monthly', 25, 3, '["Everything in Basic", "Featured listings", "Virtual tours", "Video uploads"]'),
('Enterprise', 'enterprise', 99.99, 'monthly', 100, 10, '["Everything in Premium", "Priority support", "Advanced analytics", "API access"]');
-- Insert Default Settings
INSERT INTO `settings` (`setting_key`, `setting_value`, `description`) VALUES
('site_name', 'Real Estate Platform', 'Site name'),
('site_description', 'Find your dream property with us', 'Site description'),
('contact_email', '[email protected]', 'Contact email'),
('contact_phone', '+1-555-123-4567', 'Contact phone'),
('address', '123 Real Estate Ave, Suite 100, City, State 12345', 'Business address'),
('currency', 'USD', 'Default currency'),
('currency_symbol', '$', 'Currency symbol'),
('date_format', 'M d, Y', 'Date format'),
('time_format', 'h:i A', 'Time format'),
('timezone', 'America/New_York', 'Default timezone'),
('map_api_key', '', 'Google Maps API key'),
('enable_map', '1', 'Enable Google Maps'),
('properties_per_page', '12', 'Properties per page'),
('enable_reviews', '1', 'Enable agent reviews'),
('enable_blog', '1', 'Enable blog'),
('enable_faq', '1', 'Enable FAQ'),
('facebook_url', '', 'Facebook page URL'),
('twitter_url', '', 'Twitter profile URL'),
('instagram_url', '', 'Instagram profile URL'),
('linkedin_url', '', 'LinkedIn profile URL');
-- Insert Sample Property Types
INSERT INTO `property_types` (`name`, `slug`, `icon`) VALUES
('House', 'house', 'fa-home'),
('Apartment', 'apartment', 'fa-building'),
('Condo', 'condo', 'fa-city'),
('Townhouse', 'townhouse', 'fa-house-user'),
('Land', 'land', 'fa-tree'),
('Commercial', 'commercial', 'fa-store'),
('Industrial', 'industrial', 'fa-warehouse'),
('Multi-Family', 'multi-family', 'fa-building-user');
-- Insert Sample Amenities
INSERT INTO `amenities` (`name`, `icon`, `category`) VALUES
('Air Conditioning', 'fa-wind', 'interior'),
('Heating', 'fa-fire', 'interior'),
('Hardwood Floors', 'fa-tree', 'interior'),
('Carpet', 'fa-couch', 'interior'),
('Fireplace', 'fa-fireplace', 'interior'),
('Walk-in Closet', 'fa-door-open', 'interior'),
('Washer/Dryer', 'fa-soap', 'interior'),
('Dishwasher', 'fa-utensils', 'interior'),
('Refrigerator', 'fa-refrigerator', 'interior'),
('Oven/Range', 'fa-oven', 'interior'),
('Microwave', 'fa-microwave', 'interior'),
('Disposal', 'fa-trash', 'interior'),
('Pool', 'fa-swimming-pool', 'exterior'),
('Spa/Hot Tub', 'fa-hot-tub', 'exterior'),
('Garden', 'fa-seedling', 'exterior'),
('Patio', 'fa-umbrella-beach', 'exterior'),
('Deck', 'fa-chair', 'exterior'),
('Balcony', 'fa-building', 'exterior'),
('Garage', 'fa-warehouse', 'exterior'),
('Parking', 'fa-parking', 'exterior'),
('Security System', 'fa-shield-alt', 'exterior'),
('Fenced Yard', 'fa-fence', 'exterior'),
('Clubhouse', 'fa-building', 'community'),
('Fitness Center', 'fa-dumbbell', 'community'),
('Tennis Court', 'fa-table-tennis', 'community'),
('Playground', 'fa-children', 'community'),
('Dog Park', 'fa-dog', 'community'),
('Business Center', 'fa-briefcase', 'community'),
('Elevator', 'fa-elevator', 'community'),
('High Speed Internet', 'fa-wifi', 'utilities'),
('Cable Ready', 'fa-tv', 'utilities'),
('Natural Gas', 'fa-fire', 'utilities'),
('Electricity', 'fa-bolt', 'utilities'),
('Water', 'fa-water', 'utilities'),
('Sewer', 'fa-pipe', 'utilities');
Core PHP Classes
Database Class
File: includes/Database.php
<?php
/**
* Database Class
* Handles all database connections and operations using PDO with singleton pattern
*/
class Database {
private static $instance = null;
private $connection;
private $statement;
private $host;
private $dbname;
private $username;
private $password;
/**
* Private constructor for singleton pattern
*/
private function __construct() {
$this->host = DB_HOST;
$this->dbname = DB_NAME;
$this->username = DB_USER;
$this->password = DB_PASS;
try {
$this->connection = new PDO(
"mysql:host={$this->host};dbname={$this->dbname};charset=utf8mb4",
$this->username,
$this->password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
} catch (PDOException $e) {
die("Database connection failed: " . $e->getMessage());
}
}
/**
* Get database instance (Singleton)
*/
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Get PDO connection
*/
public function getConnection() {
return $this->connection;
}
/**
* Prepare and execute query with parameters
*/
public function query($sql, $params = []) {
try {
$this->statement = $this->connection->prepare($sql);
$this->statement->execute($params);
return $this->statement;
} catch (PDOException $e) {
$this->logError($e->getMessage(), $sql, $params);
throw new Exception("Database query failed: " . $e->getMessage());
}
}
/**
* Get single row
*/
public function getRow($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->fetch();
}
/**
* Get multiple rows
*/
public function getRows($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->fetchAll();
}
/**
* Get single value
*/
public function getValue($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->fetchColumn();
}
/**
* Insert data and return last insert ID
*/
public function insert($table, $data) {
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
$this->query($sql, $data);
return $this->connection->lastInsertId();
}
/**
* Insert multiple rows
*/
public function insertMultiple($table, $data) {
if (empty($data)) {
return 0;
}
$columns = implode(', ', array_keys($data[0]));
$values = [];
$insertValues = [];
foreach ($data as $index => $row) {
$rowPlaceholders = [];
foreach (array_keys($row) as $key) {
$placeholder = ":{$key}_{$index}";
$rowPlaceholders[] = $placeholder;
$values[$placeholder] = $row[$key];
}
$insertValues[] = '(' . implode(', ', $rowPlaceholders) . ')';
}
$sql = "INSERT INTO {$table} ({$columns}) VALUES " . implode(', ', $insertValues);
$this->query($sql, $values);
return $this->connection->lastInsertId();
}
/**
* Update data
*/
public function update($table, $data, $where, $whereParams = []) {
$set = [];
foreach (array_keys($data) as $column) {
$set[] = "{$column} = :{$column}";
}
$sql = "UPDATE {$table} SET " . implode(', ', $set) . " WHERE {$where}";
$params = array_merge($data, $whereParams);
return $this->query($sql, $params)->rowCount();
}
/**
* Delete data
*/
public function delete($table, $where, $params = []) {
$sql = "DELETE FROM {$table} WHERE {$where}";
return $this->query($sql, $params)->rowCount();
}
/**
* Begin transaction
*/
public function beginTransaction() {
return $this->connection->beginTransaction();
}
/**
* Commit transaction
*/
public function commit() {
return $this->connection->commit();
}
/**
* Rollback transaction
*/
public function rollback() {
return $this->connection->rollBack();
}
/**
* Get last insert ID
*/
public function lastInsertId() {
return $this->connection->lastInsertId();
}
/**
* Check if table exists
*/
public function tableExists($table) {
$result = $this->getRow(
"SHOW TABLES LIKE ?",
[$table]
);
return !empty($result);
}
/**
* Get table columns
*/
public function getColumns($table) {
return $this->getRows("SHOW COLUMNS FROM {$table}");
}
/**
* Log database errors
*/
private function logError($message, $sql, $params) {
$logFile = __DIR__ . '/../logs/database.log';
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[{$timestamp}] Error: {$message}\n";
$logMessage .= "SQL: {$sql}\n";
$logMessage .= "Params: " . json_encode($params) . "\n";
$logMessage .= "------------------------\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
}
/**
* Prevent cloning of the instance
*/
private function __clone() {}
/**
* Prevent unserializing of the instance
*/
public function __wakeup() {}
}
?>
Configuration File
File: includes/config.php
<?php
/**
* Configuration File
* Loads environment variables and sets up constants
*/
// Start session if not started
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Load environment variables from .env file
function loadEnv($path) {
if (!file_exists($path)) {
return false;
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) {
continue;
}
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
if (!array_key_exists($name, $_ENV)) {
$_ENV[$name] = $value;
putenv(sprintf('%s=%s', $name, $value));
}
}
return true;
}
// Load environment variables
loadEnv(__DIR__ . '/../.env');
// Database Configuration
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
define('DB_NAME', getenv('DB_NAME') ?: 'real_estate');
define('DB_USER', getenv('DB_USER') ?: 'root');
define('DB_PASS', getenv('DB_PASS') ?: '');
// Application Configuration
define('APP_NAME', getenv('APP_NAME') ?: 'Real Estate Platform');
define('APP_URL', getenv('APP_URL') ?: 'http://localhost/real-estate');
define('APP_VERSION', getenv('APP_VERSION') ?: '1.0.0');
define('DEBUG_MODE', getenv('DEBUG_MODE') === 'true');
// Security Configuration
define('SESSION_TIMEOUT', getenv('SESSION_TIMEOUT') ?: 3600); // 1 hour
define('BCRYPT_ROUNDS', 12);
define('CSRF_TOKEN_NAME', 'csrf_token');
// Upload Configuration
define('UPLOAD_DIR', __DIR__ . '/../uploads/');
define('MAX_FILE_SIZE', getenv('MAX_FILE_SIZE') ?: 10 * 1024 * 1024); // 10MB
define('ALLOWED_IMAGES', ['jpg', 'jpeg', 'png', 'gif', 'webp']);
define('ALLOWED_DOCUMENTS', ['pdf', 'doc', 'docx']);
// Pagination
define('PROPERTIES_PER_PAGE', getenv('PROPERTIES_PER_PAGE') ?: 12);
define('AGENTS_PER_PAGE', getenv('AGENTS_PER_PAGE') ?: 12);
// Date/Time Configuration
date_default_timezone_set(getenv('TIMEZONE') ?: 'America/New_York');
define('DATE_FORMAT', 'M d, Y');
define('TIME_FORMAT', 'h:i A');
define('DATETIME_FORMAT', 'Y-m-d H:i:s');
// Currency Configuration
define('CURRENCY', getenv('CURRENCY') ?: 'USD');
define('CURRENCY_SYMBOL', getenv('CURRENCY_SYMBOL') ?: '$');
// Image Configuration
define('IMAGE_QUALITY', 85);
define('THUMB_WIDTH', 400);
define('THUMB_HEIGHT', 300);
define('MEDIUM_WIDTH', 800);
define('MEDIUM_HEIGHT', 600);
define('LARGE_WIDTH', 1200);
define('LARGE_HEIGHT', 900);
// Map Configuration
define('MAP_API_KEY', getenv('MAP_API_KEY') ?: '');
define('MAP_CENTER_LAT', getenv('MAP_CENTER_LAT') ?: 40.7128);
define('MAP_CENTER_LNG', getenv('MAP_CENTER_LNG') ?: -74.0060);
define('MAP_ZOOM', getenv('MAP_ZOOM') ?: 10);
// Error Reporting
if (DEBUG_MODE) {
error_reporting(E_ALL);
ini_set('display_errors', 1);
} else {
error_reporting(0);
ini_set('display_errors', 0);
}
// Include required files
require_once __DIR__ . '/Database.php';
require_once __DIR__ . '/functions.php';
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/Property.php';
require_once __DIR__ . '/User.php';
require_once __DIR__ . '/Agent.php';
require_once __DIR__ . '/Inquiry.php';
require_once __DIR__ . '/Favorite.php';
require_once __DIR__ . '/Payment.php';
require_once __DIR__ . '/Subscription.php';
require_once __DIR__ . '/Review.php';
// Initialize database connection
$db = Database::getInstance();
// Load site settings
$settings = $db->getRows("SELECT setting_key, setting_value FROM settings");
foreach ($settings as $setting) {
define(strtoupper($setting['setting_key']), $setting['setting_value']);
}
// Set timezone for MySQL
$db->query("SET time_zone = ?", [date('P')]);
?>
Helper Functions
File: includes/functions.php
<?php
/**
* Helper Functions
* Common utility functions used throughout the application
*/
/**
* Sanitize input data
*/
function sanitize($input) {
if (is_array($input)) {
return array_map('sanitize', $input);
}
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}
/**
* Generate CSRF token
*/
function generateCSRFToken() {
if (!isset($_SESSION[CSRF_TOKEN_NAME])) {
$_SESSION[CSRF_TOKEN_NAME] = bin2hex(random_bytes(32));
}
return $_SESSION[CSRF_TOKEN_NAME];
}
/**
* Verify CSRF token
*/
function verifyCSRFToken($token) {
if (!isset($_SESSION[CSRF_TOKEN_NAME]) || $token !== $_SESSION[CSRF_TOKEN_NAME]) {
return false;
}
return true;
}
/**
* Redirect to URL
*/
function redirect($url) {
header("Location: " . APP_URL . $url);
exit();
}
/**
* Format amount with currency
*/
function formatAmount($amount, $currency = null) {
if ($currency === null) {
$currency = CURRENCY;
}
$symbols = [
'USD' => '$',
'EUR' => '€',
'GBP' => '£',
'JPY' => '¥',
'INR' => '₹',
'CAD' => 'C$',
'AUD' => 'A$'
];
$symbol = $symbols[$currency] ?? CURRENCY_SYMBOL;
return $symbol . number_format($amount, 0);
}
/**
* Format date
*/
function formatDate($date, $format = null) {
if ($format === null) {
$format = DATE_FORMAT;
}
if (!$date || $date == '0000-00-00' || $date == '0000-00-00 00:00:00') {
return 'N/A';
}
return date($format, strtotime($date));
}
/**
* Format time
*/
function formatTime($time, $format = null) {
if ($format === null) {
$format = TIME_FORMAT;
}
return date($format, strtotime($time));
}
/**
* Get time ago string
*/
function timeAgo($datetime) {
$time = strtotime($datetime);
$now = time();
$diff = $now - $time;
if ($diff < 60) {
return $diff . ' seconds ago';
} elseif ($diff < 3600) {
return floor($diff / 60) . ' minutes ago';
} elseif ($diff < 86400) {
return floor($diff / 3600) . ' hours ago';
} elseif ($diff < 2592000) {
return floor($diff / 86400) . ' days ago';
} elseif ($diff < 31536000) {
return floor($diff / 2592000) . ' months ago';
} else {
return floor($diff / 31536000) . ' years ago';
}
}
/**
* Create slug from string
*/
function createSlug($string) {
$string = strtolower($string);
$string = preg_replace('/[^a-z0-9-]/', '-', $string);
$string = preg_replace('/-+/', '-', $string);
return trim($string, '-');
}
/**
* Generate unique property ID
*/
function generatePropertyId() {
return 'PROP' . date('Y') . strtoupper(uniqid());
}
/**
* Generate unique user ID
*/
function generateUserId($role) {
$prefix = strtoupper(substr($role, 0, 3));
return $prefix . date('Y') . str_pad(mt_rand(1, 9999), 4, '0', STR_PAD_LEFT);
}
/**
* Get property status badge
*/
function getStatusBadge($status) {
$badges = [
'For Sale' => 'success',
'For Rent' => 'info',
'Sold' => 'danger',
'Pending' => 'warning',
'Under Contract' => 'primary'
];
$color = $badges[$status] ?? 'secondary';
return '<span class="badge bg-' . $color . '">' . $status . '</span>';
}
/**
* Get rating stars HTML
*/
function getRatingStars($rating) {
$fullStars = floor($rating);
$halfStar = ($rating - $fullStars) >= 0.5;
$emptyStars = 5 - $fullStars - ($halfStar ? 1 : 0);
$html = '';
for ($i = 0; $i < $fullStars; $i++) {
$html .= '<i class="fas fa-star text-warning"></i>';
}
if ($halfStar) {
$html .= '<i class="fas fa-star-half-alt text-warning"></i>';
}
for ($i = 0; $i < $emptyStars; $i++) {
$html .= '<i class="far fa-star text-warning"></i>';
}
return $html;
}
/**
* Truncate text
*/
function truncateText($text, $length = 100, $suffix = '...') {
if (strlen($text) <= $length) {
return $text;
}
$truncated = substr($text, 0, $length);
$truncated = substr($truncated, 0, strrpos($truncated, ' '));
return $truncated . $suffix;
}
/**
* Format phone number
*/
function formatPhone($phone) {
$phone = preg_replace('/[^0-9]/', '', $phone);
if (strlen($phone) == 10) {
return '(' . substr($phone, 0, 3) . ') ' . substr($phone, 3, 3) . '-' . substr($phone, 6);
} elseif (strlen($phone) == 11) {
return '+' . substr($phone, 0, 1) . ' (' . substr($phone, 1, 3) . ') ' . substr($phone, 4, 3) . '-' . substr($phone, 7);
}
return $phone;
}
/**
* Get file extension
*/
function getFileExtension($filename) {
return strtolower(pathinfo($filename, PATHINFO_EXTENSION));
}
/**
* Generate unique filename
*/
function generateUniqueFilename($extension) {
return uniqid() . '_' . time() . '.' . $extension;
}
/**
* Upload file
*/
function uploadFile($file, $targetDir, $allowedTypes = null) {
if ($allowedTypes === null) {
$allowedTypes = ALLOWED_IMAGES;
}
// Check for errors
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'error' => 'Upload failed with error code: ' . $file['error']];
}
// Check file size
if ($file['size'] > MAX_FILE_SIZE) {
return ['success' => false, 'error' => 'File size exceeds limit of ' . (MAX_FILE_SIZE / 1024 / 1024) . 'MB'];
}
// Check file type
$extension = getFileExtension($file['name']);
if (!in_array($extension, $allowedTypes)) {
return ['success' => false, 'error' => 'File type not allowed. Allowed types: ' . implode(', ', $allowedTypes)];
}
// Generate unique filename
$filename = generateUniqueFilename($extension);
$targetPath = $targetDir . '/' . $filename;
// Create directory if not exists
if (!is_dir($targetDir)) {
mkdir($targetDir, 0755, true);
}
// Upload file
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
return [
'success' => true,
'filename' => $filename,
'original_name' => $file['name'],
'path' => $targetPath,
'extension' => $extension,
'size' => $file['size']
];
}
return ['success' => false, 'error' => 'Failed to move uploaded file'];
}
/**
* Delete file
*/
function deleteFile($path) {
if (file_exists($path)) {
return unlink($path);
}
return false;
}
/**
* Get user IP address
*/
function getUserIP() {
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
return $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
return $_SERVER['REMOTE_ADDR'];
}
}
/**
* Generate random string
*/
function generateRandomString($length = 32) {
return bin2hex(random_bytes($length / 2));
}
/**
* Validate email
*/
function validateEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL);
}
/**
* Validate phone number
*/
function validatePhone($phone) {
return preg_match('/^[0-9\-\(\)\/\+\s]+$/', $phone);
}
/**
* Validate URL
*/
function validateURL($url) {
return filter_var($url, FILTER_VALIDATE_URL);
}
/**
* Send email notification
*/
function sendEmail($to, $subject, $message, $from = null) {
if ($from === null) {
$from = CONTACT_EMAIL;
}
$headers = [
'MIME-Version: 1.0',
'Content-type: text/html; charset=utf-8',
'From: ' . SITE_NAME . ' <' . $from . '>',
'Reply-To: ' . $from,
'X-Mailer: PHP/' . phpversion()
];
return mail($to, $subject, $message, implode("\r\n", $headers));
}
/**
* Get pagination links
*/
function paginate($currentPage, $totalPages, $url) {
if ($totalPages <= 1) {
return '';
}
$html = '<nav aria-label="Page navigation"><ul class="pagination">';
// Previous button
if ($currentPage > 1) {
$html .= '<li class="page-item"><a class="page-link" href="' . $url . '?page=' . ($currentPage - 1) . '">Previous</a></li>';
} else {
$html .= '<li class="page-item disabled"><span class="page-link">Previous</span></li>';
}
// Page numbers
for ($i = 1; $i <= $totalPages; $i++) {
if ($i == $currentPage) {
$html .= '<li class="page-item active"><span class="page-link">' . $i . '</span></li>';
} else {
$html .= '<li class="page-item"><a class="page-link" href="' . $url . '?page=' . $i . '">' . $i . '</a></li>';
}
}
// Next button
if ($currentPage < $totalPages) {
$html .= '<li class="page-item"><a class="page-link" href="' . $url . '?page=' . ($currentPage + 1) . '">Next</a></li>';
} else {
$html .= '<li class="page-item disabled"><span class="page-link">Next</span></li>';
}
$html .= '</ul></nav>';
return $html;
}
/**
* Log error
*/
function logError($message, $context = []) {
$logFile = __DIR__ . '/../logs/error.log';
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$timestamp = date('Y-m-d H:i:s');
$contextStr = !empty($context) ? ' ' . json_encode($context) : '';
$logMessage = "[{$timestamp}] {$message}{$contextStr}\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
}
/**
* Get month name
*/
function getMonthName($month) {
$months = [
1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April',
5 => 'May', 6 => 'June', 7 => 'July', 8 => 'August',
9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December'
];
return $months[$month] ?? '';
}
/**
* Calculate mortgage payment
*/
function calculateMortgage($principal, $interestRate, $years, $downPayment = 0) {
$loanAmount = $principal - $downPayment;
$monthlyRate = ($interestRate / 100) / 12;
$totalPayments = $years * 12;
if ($monthlyRate == 0) {
$monthlyPayment = $loanAmount / $totalPayments;
} else {
$monthlyPayment = $loanAmount * $monthlyRate * pow(1 + $monthlyRate, $totalPayments) / (pow(1 + $monthlyRate, $totalPayments) - 1);
}
return [
'monthly' => round($monthlyPayment, 2),
'total' => round($monthlyPayment * $totalPayments, 2),
'interest' => round(($monthlyPayment * $totalPayments) - $loanAmount, 2),
'loan_amount' => round($loanAmount, 2),
'down_payment' => round($downPayment, 2)
];
}
?>
Property Class
File: includes/Property.php
<?php
/**
* Property Class
* Handles all property-related operations
*/
class Property {
private $db;
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Create new property
*/
public function create($data) {
try {
$this->db->beginTransaction();
// Generate property ID and slug
$propertyId = generatePropertyId();
$slug = createSlug($data['title']) . '-' . $propertyId;
// Prepare property data
$propertyData = [
'property_id' => $propertyId,
'slug' => $slug,
'title' => $data['title'],
'description' => $data['description'],
'property_type_id' => $data['property_type_id'],
'status_id' => $data['status_id'],
'price' => $data['price'],
'bedrooms' => $data['bedrooms'] ?? null,
'bathrooms' => $data['bathrooms'] ?? null,
'sqft' => $data['sqft'] ?? null,
'lot_size' => $data['lot_size'] ?? null,
'year_built' => $data['year_built'] ?? null,
'address' => $data['address'],
'city' => $data['city'],
'state' => $data['state'],
'zipcode' => $data['zipcode'],
'country' => $data['country'] ?? 'USA',
'latitude' => $data['latitude'] ?? null,
'longitude' => $data['longitude'] ?? null,
'neighborhood' => $data['neighborhood'] ?? null,
'school_district' => $data['school_district'] ?? null,
'is_virtual_tour' => !empty($data['virtual_tour_url']),
'virtual_tour_url' => $data['virtual_tour_url'] ?? null,
'video_url' => $data['video_url'] ?? null
];
// Add agent/seller based on role
if (isset($data['agent_id'])) {
$propertyData['agent_id'] = $data['agent_id'];
}
if (isset($data['seller_id'])) {
$propertyData['seller_id'] = $data['seller_id'];
}
// Insert property
$propertyDbId = $this->db->insert('properties', $propertyData);
// Add amenities
if (!empty($data['amenities'])) {
$this->addAmenities($propertyDbId, $data['amenities']);
}
// Upload images
if (!empty($_FILES['images'])) {
$this->uploadImages($propertyDbId, $_FILES['images']);
}
$this->db->commit();
return [
'success' => true,
'property_id' => $propertyId,
'id' => $propertyDbId,
'message' => 'Property created successfully'
];
} catch (Exception $e) {
$this->db->rollback();
logError('Create property error: ' . $e->getMessage(), $data);
return ['success' => false, 'error' => 'Failed to create property: ' . $e->getMessage()];
}
}
/**
* Update property
*/
public function update($propertyId, $data) {
try {
$property = $this->getById($propertyId);
if (!$property) {
return ['success' => false, 'error' => 'Property not found'];
}
$this->db->beginTransaction();
// Prepare update data
$updateData = [
'title' => $data['title'],
'description' => $data['description'],
'property_type_id' => $data['property_type_id'],
'status_id' => $data['status_id'],
'price' => $data['price'],
'bedrooms' => $data['bedrooms'] ?? null,
'bathrooms' => $data['bathrooms'] ?? null,
'sqft' => $data['sqft'] ?? null,
'lot_size' => $data['lot_size'] ?? null,
'year_built' => $data['year_built'] ?? null,
'address' => $data['address'],
'city' => $data['city'],
'state' => $data['state'],
'zipcode' => $data['zipcode'],
'country' => $data['country'] ?? 'USA',
'latitude' => $data['latitude'] ?? null,
'longitude' => $data['longitude'] ?? null,
'neighborhood' => $data['neighborhood'] ?? null,
'school_district' => $data['school_district'] ?? null,
'is_virtual_tour' => !empty($data['virtual_tour_url']),
'virtual_tour_url' => $data['virtual_tour_url'] ?? null,
'video_url' => $data['video_url'] ?? null
];
// Update slug if title changed
if ($property['title'] != $data['title']) {
$updateData['slug'] = createSlug($data['title']) . '-' . $property['property_id'];
}
// Update property
$this->db->update(
'properties',
$updateData,
'id = :id',
['id' => $property['id']]
);
// Update amenities
$this->db->delete('property_amenities', 'property_id = ?', [$property['id']]);
if (!empty($data['amenities'])) {
$this->addAmenities($property['id'], $data['amenities']);
}
// Upload new images
if (!empty($_FILES['images'])) {
$this->uploadImages($property['id'], $_FILES['images']);
}
$this->db->commit();
return [
'success' => true,
'message' => 'Property updated successfully'
];
} catch (Exception $e) {
$this->db->rollback();
logError('Update property error: ' . $e->getMessage(), $data);
return ['success' => false, 'error' => 'Failed to update property'];
}
}
/**
* Delete property
*/
public function delete($propertyId) {
try {
$property = $this->getById($propertyId);
if (!$property) {
return ['success' => false, 'error' => 'Property not found'];
}
// Delete images from filesystem
$images = $this->getImages($property['id']);
foreach ($images as $image) {
$path = UPLOAD_DIR . 'properties/' . $image['image_path'];
deleteFile($path);
// Delete thumbnails
$thumbPath = UPLOAD_DIR . 'properties/thumbs/' . $image['image_path'];
deleteFile($thumbPath);
}
// Delete from database
$this->db->delete('properties', 'id = ?', [$property['id']]);
return ['success' => true, 'message' => 'Property deleted successfully'];
} catch (Exception $e) {
logError('Delete property error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to delete property'];
}
}
/**
* Get property by ID
*/
public function getById($id) {
return $this->db->getRow(
"SELECT p.*, pt.name as property_type_name, ps.name as status_name, ps.color as status_color,
a.id as agent_id, a.user_id as agent_user_id, a.business_name,
u.first_name as agent_first_name, u.last_name as agent_last_name, u.email as agent_email,
u.phone as agent_phone, u.profile_picture as agent_image,
(SELECT COUNT(*) FROM property_views WHERE property_id = p.id) as view_count,
(SELECT COUNT(*) FROM favorites WHERE property_id = p.id) as favorite_count
FROM properties p
LEFT JOIN property_types pt ON p.property_type_id = pt.id
LEFT JOIN property_status ps ON p.status_id = ps.id
LEFT JOIN agents a ON p.agent_id = a.id
LEFT JOIN users u ON a.user_id = u.id
WHERE p.id = ? OR p.property_id = ?",
[$id, $id]
);
}
/**
* Get property by slug
*/
public function getBySlug($slug) {
return $this->db->getRow(
"SELECT p.*, pt.name as property_type_name, ps.name as status_name, ps.color as status_color,
a.id as agent_id, a.business_name,
u.first_name as agent_first_name, u.last_name as agent_last_name, u.email as agent_email,
u.phone as agent_phone, u.profile_picture as agent_image
FROM properties p
LEFT JOIN property_types pt ON p.property_type_id = pt.id
LEFT JOIN property_status ps ON p.status_id = ps.id
LEFT JOIN agents a ON p.agent_id = a.id
LEFT JOIN users u ON a.user_id = u.id
WHERE p.slug = ?",
[$slug]
);
}
/**
* Get properties with filters
*/
public function getProperties($filters = []) {
$sql = "SELECT p.*, pt.name as property_type_name, ps.name as status_name, ps.color as status_color,
(SELECT image_path FROM property_images WHERE property_id = p.id AND is_primary = 1 LIMIT 1) as primary_image
FROM properties p
LEFT JOIN property_types pt ON p.property_type_id = pt.id
LEFT JOIN property_status ps ON p.status_id = ps.id
WHERE p.is_active = 1 AND p.is_approved = 1";
$params = [];
// Apply filters
if (!empty($filters['property_type'])) {
$sql .= " AND p.property_type_id = :property_type";
$params['property_type'] = $filters['property_type'];
}
if (!empty($filters['status'])) {
$sql .= " AND p.status_id = :status";
$params['status'] = $filters['status'];
}
if (!empty($filters['city'])) {
$sql .= " AND p.city LIKE :city";
$params['city'] = '%' . $filters['city'] . '%';
}
if (!empty($filters['state'])) {
$sql .= " AND p.state = :state";
$params['state'] = $filters['state'];
}
if (!empty($filters['zipcode'])) {
$sql .= " AND p.zipcode = :zipcode";
$params['zipcode'] = $filters['zipcode'];
}
if (!empty($filters['min_price'])) {
$sql .= " AND p.price >= :min_price";
$params['min_price'] = $filters['min_price'];
}
if (!empty($filters['max_price'])) {
$sql .= " AND p.price <= :max_price";
$params['max_price'] = $filters['max_price'];
}
if (!empty($filters['min_bedrooms'])) {
$sql .= " AND p.bedrooms >= :min_bedrooms";
$params['min_bedrooms'] = $filters['min_bedrooms'];
}
if (!empty($filters['min_bathrooms'])) {
$sql .= " AND p.bathrooms >= :min_bathrooms";
$params['min_bathrooms'] = $filters['min_bathrooms'];
}
if (!empty($filters['min_sqft'])) {
$sql .= " AND p.sqft >= :min_sqft";
$params['min_sqft'] = $filters['min_sqft'];
}
if (!empty($filters['featured'])) {
$sql .= " AND p.featured = 1";
}
if (!empty($filters['agent_id'])) {
$sql .= " AND p.agent_id = :agent_id";
$params['agent_id'] = $filters['agent_id'];
}
if (!empty($filters['seller_id'])) {
$sql .= " AND p.seller_id = :seller_id";
$params['seller_id'] = $filters['seller_id'];
}
if (!empty($filters['amenities'])) {
$amenityCount = count($filters['amenities']);
$sql .= " AND p.id IN (
SELECT property_id FROM property_amenities
WHERE amenity_id IN (" . implode(',', array_fill(0, $amenityCount, '?')) . ")
GROUP BY property_id
HAVING COUNT(DISTINCT amenity_id) = ?
)";
$params = array_merge($params, $filters['amenities'], [$amenityCount]);
}
// Search
if (!empty($filters['search'])) {
$sql .= " AND (p.title LIKE :search OR p.description LIKE :search OR p.address LIKE :search OR p.city LIKE :search)";
$params['search'] = '%' . $filters['search'] . '%';
}
// Order by
$orderBy = $filters['order_by'] ?? 'created_at';
$orderDir = $filters['order_dir'] ?? 'DESC';
$sql .= " ORDER BY p.{$orderBy} {$orderDir}";
// Pagination
$page = $filters['page'] ?? 1;
$limit = $filters['limit'] ?? PROPERTIES_PER_PAGE;
$offset = ($page - 1) * $limit;
$sql .= " LIMIT :limit OFFSET :offset";
$params['limit'] = $limit;
$params['offset'] = $offset;
return $this->db->getRows($sql, $params);
}
/**
* Get property count
*/
public function getCount($filters = []) {
$sql = "SELECT COUNT(*) as count FROM properties WHERE is_active = 1 AND is_approved = 1";
$params = [];
// Apply same filters as getProperties
if (!empty($filters['property_type'])) {
$sql .= " AND property_type_id = :property_type";
$params['property_type'] = $filters['property_type'];
}
if (!empty($filters['status'])) {
$sql .= " AND status_id = :status";
$params['status'] = $filters['status'];
}
if (!empty($filters['city'])) {
$sql .= " AND city LIKE :city";
$params['city'] = '%' . $filters['city'] . '%';
}
if (!empty($filters['min_price'])) {
$sql .= " AND price >= :min_price";
$params['min_price'] = $filters['min_price'];
}
if (!empty($filters['max_price'])) {
$sql .= " AND price <= :max_price";
$params['max_price'] = $filters['max_price'];
}
if (!empty($filters['search'])) {
$sql .= " AND (title LIKE :search OR description LIKE :search)";
$params['search'] = '%' . $filters['search'] . '%';
}
$result = $this->db->getRow($sql, $params);
return $result['count'];
}
/**
* Get featured properties
*/
public function getFeatured($limit = 6) {
return $this->getProperties([
'featured' => true,
'limit' => $limit,
'order_by' => 'created_at',
'order_dir' => 'DESC'
]);
}
/**
* Get recent properties
*/
public function getRecent($limit = 6) {
return $this->getProperties([
'limit' => $limit,
'order_by' => 'created_at',
'order_dir' => 'DESC'
]);
}
/**
* Get similar properties
*/
public function getSimilar($propertyId, $limit = 3) {
$property = $this->getById($propertyId);
if (!$property) {
return [];
}
return $this->getProperties([
'property_type' => $property['property_type_id'],
'city' => $property['city'],
'min_price' => $property['price'] * 0.7,
'max_price' => $property['price'] * 1.3,
'limit' => $limit,
'exclude_id' => $property['id']
]);
}
/**
* Get properties by agent
*/
public function getByAgent($agentId, $limit = null) {
return $this->getProperties([
'agent_id' => $agentId,
'limit' => $limit
]);
}
/**
* Get properties by seller
*/
public function getBySeller($sellerId, $limit = null) {
return $this->getProperties([
'seller_id' => $sellerId,
'limit' => $limit
]);
}
/**
* Get property images
*/
public function getImages($propertyId) {
return $this->db->getRows(
"SELECT * FROM property_images
WHERE property_id = ?
ORDER BY is_primary DESC, sort_order ASC",
[$propertyId]
);
}
/**
* Get primary image
*/
public function getPrimaryImage($propertyId) {
return $this->db->getRow(
"SELECT * FROM property_images
WHERE property_id = ? AND is_primary = 1",
[$propertyId]
);
}
/**
* Get property amenities
*/
public function getAmenities($propertyId) {
return $this->db->getRows(
"SELECT a.* FROM amenities a
JOIN property_amenities pa ON a.id = pa.amenity_id
WHERE pa.property_id = ?
ORDER BY a.category, a.sort_order",
[$propertyId]
);
}
/**
* Add amenities to property
*/
private function addAmenities($propertyId, $amenities) {
$data = [];
foreach ($amenities as $amenityId) {
$data[] = [
'property_id' => $propertyId,
'amenity_id' => $amenityId
];
}
if (!empty($data)) {
$this->db->insertMultiple('property_amenities', $data);
}
}
/**
* Upload property images
*/
private function uploadImages($propertyId, $files) {
require_once __DIR__ . '/helpers/ImageHelper.php';
$imageHelper = new ImageHelper();
$uploadDir = UPLOAD_DIR . 'properties/';
$uploadedCount = 0;
foreach ($files['tmp_name'] as $key => $tmpName) {
if ($files['error'][$key] !== UPLOAD_ERR_OK) {
continue;
}
$file = [
'name' => $files['name'][$key],
'type' => $files['type'][$key],
'tmp_name' => $tmpName,
'error' => $files['error'][$key],
'size' => $files['size'][$key]
];
$result = $imageHelper->uploadPropertyImage($file, $uploadDir);
if ($result['success']) {
$isPrimary = ($uploadedCount === 0); // First image is primary
$this->db->insert('property_images', [
'property_id' => $propertyId,
'image_path' => $result['filename'],
'is_primary' => $isPrimary,
'sort_order' => $uploadedCount
]);
$uploadedCount++;
}
}
return $uploadedCount;
}
/**
* Set primary image
*/
public function setPrimaryImage($propertyId, $imageId) {
// Reset all images to non-primary
$this->db->update(
'property_images',
['is_primary' => false],
'property_id = :property_id',
['property_id' => $propertyId]
);
// Set selected image as primary
return $this->db->update(
'property_images',
['is_primary' => true],
'id = :id AND property_id = :property_id',
['id' => $imageId, 'property_id' => $propertyId]
);
}
/**
* Delete image
*/
public function deleteImage($imageId, $propertyId) {
$image = $this->db->getRow(
"SELECT * FROM property_images WHERE id = ? AND property_id = ?",
[$imageId, $propertyId]
);
if (!$image) {
return false;
}
// Delete file
$path = UPLOAD_DIR . 'properties/' . $image['image_path'];
deleteFile($path);
// Delete thumbnail
$thumbPath = UPLOAD_DIR . 'properties/thumbs/' . $image['image_path'];
deleteFile($thumbPath);
// Delete from database
return $this->db->delete('property_images', 'id = ?', [$imageId]);
}
/**
* Increment view count
*/
public function incrementViews($propertyId) {
$this->db->query(
"UPDATE properties SET views = views + 1 WHERE id = ?",
[$propertyId]
);
// Log view
$this->db->insert('property_views', [
'property_id' => $propertyId,
'user_id' => $_SESSION['user_id'] ?? null,
'ip_address' => getUserIP(),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null
]);
}
/**
* Get property statistics
*/
public function getStatistics($propertyId) {
return $this->db->getRow(
"SELECT
COUNT(DISTINCT pv.id) as total_views,
COUNT(DISTINCT f.id) as total_favorites,
COUNT(DISTINCT i.id) as total_inquiries,
COUNT(DISTINCT v.id) as total_viewings
FROM properties p
LEFT JOIN property_views pv ON p.id = pv.property_id
LEFT JOIN favorites f ON p.id = f.property_id
LEFT JOIN inquiries i ON p.id = i.property_id
LEFT JOIN viewings v ON p.id = v.property_id
WHERE p.id = ?",
[$propertyId]
);
}
/**
* Get property types
*/
public function getPropertyTypes() {
return $this->db->getRows(
"SELECT * FROM property_types WHERE is_active = 1 ORDER BY sort_order"
);
}
/**
* Get property statuses
*/
public function getStatuses() {
return $this->db->getRows(
"SELECT * FROM property_status WHERE is_active = 1 ORDER BY sort_order"
);
}
/**
* Get all amenities grouped by category
*/
public function getAllAmenities() {
return $this->db->getRows(
"SELECT * FROM amenities WHERE is_active = 1 ORDER BY category, sort_order"
);
}
/**
* Get amenities by category
*/
public function getAmenitiesByCategory() {
$amenities = $this->getAllAmenities();
$grouped = [];
foreach ($amenities as $amenity) {
$grouped[$amenity['category']][] = $amenity;
}
return $grouped;
}
/**
* Get cities list
*/
public function getCities() {
return $this->db->getRows(
"SELECT DISTINCT city, COUNT(*) as property_count
FROM properties
WHERE is_active = 1 AND is_approved = 1
GROUP BY city
ORDER BY city"
);
}
/**
* Get price ranges for filter
*/
public function getPriceRanges() {
return [
['min' => 0, 'max' => 100000, 'label' => 'Under $100k'],
['min' => 100000, 'max' => 200000, 'label' => '$100k - $200k'],
['min' => 200000, 'max' => 300000, 'label' => '$200k - $300k'],
['min' => 300000, 'max' => 400000, 'label' => '$300k - $400k'],
['min' => 400000, 'max' => 500000, 'label' => '$400k - $500k'],
['min' => 500000, 'max' => 750000, 'label' => '$500k - $750k'],
['min' => 750000, 'max' => 1000000, 'label' => '$750k - $1M'],
['min' => 1000000, 'max' => 2000000, 'label' => '$1M - $2M'],
['min' => 2000000, 'max' => null, 'label' => '$2M+']
];
}
}
?>
Authentication Class
File: includes/auth.php
<?php
/**
* Authentication Class
* Handles user authentication, registration, and session management
*/
class Auth {
private $db;
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Register new user
*/
public function register($data) {
try {
// Check if email already exists
$existing = $this->db->getRow(
"SELECT id FROM users WHERE email = ?",
[$data['email']]
);
if ($existing) {
return ['success' => false, 'error' => 'Email already registered'];
}
// Hash password
$hashedPassword = password_hash($data['password'], PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
// Generate user ID
$userId = generateUserId($data['role'] ?? 'buyer');
// Generate verification token
$verificationToken = generateRandomString();
// Prepare user data
$userData = [
'user_id' => $userId,
'email' => $data['email'],
'password' => $hashedPassword,
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'phone' => $data['phone'] ?? null,
'role' => $data['role'] ?? 'buyer',
'status' => 'pending',
'verification_token' => $verificationToken
];
// Insert user
$newUserId = $this->db->insert('users', $userData);
if ($newUserId) {
// If role is agent, create agent record
if ($data['role'] === 'agent') {
$this->createAgentRecord($newUserId, $data);
}
// Send verification email
$this->sendVerificationEmail($data['email'], $verificationToken);
return [
'success' => true,
'user_id' => $newUserId,
'message' => 'Registration successful. Please check your email to verify your account.'
];
}
return ['success' => false, 'error' => 'Registration failed'];
} catch (Exception $e) {
logError('Registration error: ' . $e->getMessage(), $data);
return ['success' => false, 'error' => 'Registration failed: ' . $e->getMessage()];
}
}
/**
* Create agent record
*/
private function createAgentRecord($userId, $data) {
$agentData = [
'user_id' => $userId,
'license_number' => $data['license_number'] ?? null,
'agency_name' => $data['agency_name'] ?? null,
'years_experience' => $data['years_experience'] ?? 0,
'specializations' => $data['specializations'] ?? null,
'bio' => $data['bio'] ?? null,
'subscription_plan' => 'basic',
'subscription_expires' => date('Y-m-d', strtotime('+30 days'))
];
$this->db->insert('agents', $agentData);
}
/**
* Login user
*/
public function login($email, $password, $remember = false) {
try {
// Get user
$user = $this->db->getRow(
"SELECT * FROM users WHERE email = ?",
[$email]
);
if (!$user) {
return ['success' => false, 'error' => 'Invalid email or password'];
}
// Check status
if ($user['status'] === 'suspended') {
return ['success' => false, 'error' => 'Your account has been suspended. Please contact support.'];
}
if ($user['status'] === 'pending') {
return ['success' => false, 'error' => 'Please verify your email before logging in'];
}
// Verify password
if (!password_verify($password, $user['password'])) {
return ['success' => false, 'error' => 'Invalid email or password'];
}
// Check if password needs rehash
if (password_needs_rehash($user['password'], PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS])) {
$newHash = password_hash($password, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
$this->db->update('users', ['password' => $newHash], 'id = :id', ['id' => $user['id']]);
}
// Set session
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_email'] = $user['email'];
$_SESSION['user_name'] = $user['first_name'] . ' ' . $user['last_name'];
$_SESSION['user_role'] = $user['role'];
$_SESSION['user_id_display'] = $user['user_id'];
$_SESSION['logged_in'] = true;
$_SESSION['login_time'] = time();
// Update last login
$this->db->update(
'users',
['last_login' => date('Y-m-d H:i:s')],
'id = :id',
['id' => $user['id']]
);
// Set remember me cookie
if ($remember) {
$this->setRememberMe($user['id']);
}
// If agent, get agent details
if ($user['role'] === 'agent') {
$agent = $this->db->getRow(
"SELECT id FROM agents WHERE user_id = ?",
[$user['id']]
);
$_SESSION['agent_id'] = $agent['id'];
}
return ['success' => true, 'user' => $user];
} catch (Exception $e) {
logError('Login error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Login failed'];
}
}
/**
* Set remember me cookie
*/
private function setRememberMe($userId) {
$token = generateRandomString(64);
$expires = time() + (86400 * 30); // 30 days
// Store token in database (you would need a remember_tokens table)
// For now, just set cookie
setcookie('remember_token', $token, $expires, '/', '', false, true);
}
/**
* Logout user
*/
public function logout() {
// Clear session
$_SESSION = array();
// Clear session cookie
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
// Clear remember me cookie
setcookie('remember_token', '', time() - 3600, '/');
// Destroy session
session_destroy();
}
/**
* Check if user is logged in
*/
public function isLoggedIn() {
return isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true;
}
/**
* Get current user
*/
public function getCurrentUser() {
if (!$this->isLoggedIn()) {
return null;
}
return $this->db->getRow(
"SELECT * FROM users WHERE id = ?",
[$_SESSION['user_id']]
);
}
/**
* Check if user has role
*/
public function hasRole($role) {
return isset($_SESSION['user_role']) && $_SESSION['user_role'] === $role;
}
/**
* Check session timeout
*/
public function checkSessionTimeout() {
if ($this->isLoggedIn() && (time() - $_SESSION['login_time']) > SESSION_TIMEOUT) {
$this->logout();
return false;
}
return true;
}
/**
* Require login
*/
public function requireLogin() {
if (!$this->isLoggedIn()) {
$_SESSION['error'] = 'Please login to access this page';
redirect('/login.php');
}
$this->checkSessionTimeout();
}
/**
* Require role
*/
public function requireRole($role) {
$this->requireLogin();
if (!$this->hasRole($role)) {
$_SESSION['error'] = 'You do not have permission to access this page';
// Redirect based on role
if ($this->hasRole('admin')) {
redirect('/admin/dashboard.php');
} elseif ($this->hasRole('agent')) {
redirect('/agent/dashboard.php');
} elseif ($this->hasRole('seller')) {
redirect('/seller/dashboard.php');
} else {
redirect('/buyer/dashboard.php');
}
}
}
/**
* Verify email
*/
public function verifyEmail($token) {
$user = $this->db->getRow(
"SELECT id FROM users WHERE verification_token = ?",
[$token]
);
if ($user) {
$this->db->update(
'users',
['email_verified' => true, 'status' => 'active', 'verification_token' => null],
'id = :id',
['id' => $user['id']]
);
return true;
}
return false;
}
/**
* Send verification email
*/
private function sendVerificationEmail($email, $token) {
$subject = "Verify your email - " . SITE_NAME;
$message = "
<h2>Welcome to " . SITE_NAME . "!</h2>
<p>Please click the link below to verify your email address:</p>
<p><a href='" . APP_URL . "/verify.php?token=" . $token . "'>Verify Email</a></p>
<p>If you didn't create an account, you can ignore this email.</p>
";
return sendEmail($email, $subject, $message);
}
/**
* Forgot password
*/
public function forgotPassword($email) {
$user = $this->db->getRow(
"SELECT id FROM users WHERE email = ?",
[$email]
);
if ($user) {
$token = generateRandomString();
$expires = date('Y-m-d H:i:s', strtotime('+1 hour'));
$this->db->update(
'users',
['reset_token' => $token, 'reset_expires' => $expires],
'id = :id',
['id' => $user['id']]
);
// Send reset email
$subject = "Password Reset - " . SITE_NAME;
$message = "
<h2>Password Reset Request</h2>
<p>Click the link below to reset your password:</p>
<p><a href='" . APP_URL . "/reset_password.php?token=" . $token . "'>Reset Password</a></p>
<p>This link will expire in 1 hour.</p>
<p>If you didn't request this, please ignore this email.</p>
";
return sendEmail($email, $subject, $message);
}
return false;
}
/**
* Reset password
*/
public function resetPassword($token, $password) {
$user = $this->db->getRow(
"SELECT id FROM users WHERE reset_token = ? AND reset_expires > NOW()",
[$token]
);
if ($user) {
$hashedPassword = password_hash($password, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
$this->db->update(
'users',
['password' => $hashedPassword, 'reset_token' => null, 'reset_expires' => null],
'id = :id',
['id' => $user['id']]
);
return true;
}
return false;
}
/**
* Update user profile
*/
public function updateProfile($userId, $data) {
try {
$updateData = [
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'phone' => $data['phone'] ?? null
];
// Handle profile picture upload
if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] == 0) {
require_once __DIR__ . '/helpers/ImageHelper.php';
$imageHelper = new ImageHelper();
$uploadDir = UPLOAD_DIR . 'users/';
$result = $imageHelper->uploadProfileImage($_FILES['profile_picture'], $uploadDir);
if ($result['success']) {
$updateData['profile_picture'] = $result['filename'];
}
}
$this->db->update('users', $updateData, 'id = :id', ['id' => $userId]);
return ['success' => true, 'message' => 'Profile updated successfully'];
} catch (Exception $e) {
logError('Profile update error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to update profile'];
}
}
/**
* Change password
*/
public function changePassword($userId, $currentPassword, $newPassword) {
$user = $this->db->getRow("SELECT password FROM users WHERE id = ?", [$userId]);
if (!password_verify($currentPassword, $user['password'])) {
return ['success' => false, 'error' => 'Current password is incorrect'];
}
$hashedPassword = password_hash($newPassword, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
$this->db->update('users', ['password' => $hashedPassword], 'id = :id', ['id' => $userId]);
return ['success' => true, 'message' => 'Password changed successfully'];
}
}
// Initialize Auth
$auth = new Auth();
?>
Frontend Pages
Main Landing Page
File: index.php
```php
<?php
require_once 'includes/config.php';
// Initialize classes
$property = new Property();
// Get featured properties
$featuredProperties = $property->getFeatured(6);
// Get recent properties
$recentProperties = $property->getRecent(6);
// Get property types for navigation
$propertyTypes = $property->getPropertyTypes();
// Get cities for search
$cities = $property->getCities();
?>
<?php echo SITE_NAME; ?> - Find Your Dream Property
<!-- Bootstrap 5 CSS --> <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> <!-- Font Awesome --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <!-- Select2 CSS --> <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css" rel="stylesheet" /> <!-- Custom CSS --> <link rel="stylesheet" href="assets/css/style.css">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav ms-auto"> <li class="nav-item"> <a class="nav-link active" href="index.php">Home</a> </li> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" id="propertiesDropdown" role="button" data-bs-toggle="dropdown"> Properties </a> <ul class="dropdown-menu"> <?php foreach ($propertyTypes as $type): ?> <li> <a class="dropdown-item" href="properties.php?type=<?php echo $type['id']; ?>"> <i class="fas <?php echo $type['icon']; ?> me-2"></i> <?php echo $type['name']; ?> </a> </li> <?php endforeach; ?> <li><hr class="dropdown-divider"></li> <li><a class="dropdown-item" href="properties.php">All Properties</a></li> </ul> </li> <li class="nav-item"> <a class="nav-link" href="agents.php">Agents</a> </li> <li class="nav-item"> <a class="nav-link" href="about.php">About</a> </li> <li class="nav-item"> <a class="nav-link" href="contact.php">Contact</a> </li> <?php if ($auth->isLoggedIn()): ?> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown"> <i class="fas fa-user-circle me-1"></i> <?php echo htmlspecialchars($_SESSION['user_name']); ?> </a> <ul class="dropdown-menu dropdown-menu-end"> <li> <a class="dropdown-item" href="<?php echo $_SESSION['user_role'] == 'admin' ? 'admin/dashboard.php' : ($_SESSION['user_role'] == 'agent' ? 'agent/dashboard.php' : ($_SESSION['user_role'] == 'seller' ? 'seller/dashboard.php' : 'buyer/dashboard.php')); ?>"> <i class="fas fa-tachometer-alt me-2"></i>Dashboard </a> </li> <li> <a class="dropdown-item" href="favorites.php"> <i class="fas fa-heart me-2"></i>Saved Properties </a> </li> <li><hr class="dropdown-divider"></li> <li> <a class="dropdown-item text-danger" href="logout.php"> <i class="fas fa-sign-out-alt me-2"></i>Logout </a> </li> </ul> </li> <?php else: ?> <li class="nav-item"> <a class="nav-link" href="login.php">Login</a> </li> <li class="nav-item"> <a class="btn btn-primary ms-2" href="register.php">Sign Up</a> </li> <?php endif; ?> </ul> </div> </div> </nav> <!-- Hero Section with Search --> <section class="hero-section bg-primary text-white py-5 mt-5"> <div class="container py-5"> <div class="row justify-content-center text-center"> <div class="col-lg-8"> <h1 class="display-4 fw-bold mb-4">Find Your Dream Property</h1> <p class="lead mb-5">Search thousands of listings for sale and rent across the country</p> <!-- Search Form --> <div class="search-box bg-white p-4 rounded-4 shadow"> <form action="properties.php" method="GET" class="row g-3"> <div class="col-md-5"> <div class="input-group"> <span class="input-group-text bg-white border-end-0"> <i class="fas fa-search text-primary"></i> </span> <input type="text" class="form-control border-start-0" name="search" placeholder="Search by location, address, or ZIP"> </div> </div> <div class="col-md-3"> <select class="form-select" name="type"> <option value="">Property Type</option> <?php foreach ($propertyTypes as $type): ?> <option value="<?php echo $type['id']; ?>"> <?php echo $type['name']; ?> </option> <?php endforeach; ?> </select> </div> <div class="col-md-2"> <select class="form-select" name="status"> <option value="">Status</option> <option value="1">For Sale</option> <option value="2">For Rent</option> </select> </div> <div class="col-md-2"> <button type="submit" class="btn btn-primary w-100"> <i class="fas fa-search me-2"></i>Search </button> </div> </form> </div> <!-- Popular Cities --> <div class="mt-4"> <p class="mb-2">Popular Cities:</p> <div class="d-flex flex-wrap justify-content-center gap-2"> <?php $topCities = array_slice($cities, 0, 6); foreach ($topCities as $city): ?> <a href="properties.php?city=<?php echo urlencode($city['city']); ?>" class="badge bg-light text-dark text-decoration-none p-2"> <?php echo $city['city']; ?> (<?php echo $city['property_count']; ?>) </a> <?php endforeach; ?> </div> </div> </div> </div> </div> </section> <!-- Featured Properties --> <section class="py-5"> <div class="container"> <div class="d-flex justify-content-between align-items-center mb-4"> <h2 class="fw-bold">Featured Properties</h2> <a href="properties.php?featured=1" class="btn btn-outline-primary"> View All <i class="fas fa-arrow-right ms-2"></i> </a> </div> <div class="row g-4"> <?php foreach ($featuredProperties as $property): ?> <div class="col-lg-4 col-md-6"> <div class="card property-card h-100 border-0 shadow-sm"> <div class="position-relative"> <img src="uploads/properties/<?php echo $property['primary_image'] ?? 'default.jpg'; ?>" class="card-img-top" alt="<?php echo htmlspecialchars($property['title']); ?>"> <?php if ($property['featured']): ?> <span class="badge bg-warning position-absolute top-0 start-0 m-3"> <i class="fas fa-star me-1"></i>Featured </span> <?php endif; ?> <span class="badge bg-<?php echo $property['status_color']; ?> position-absolute top-0 end-0 m-3"> <?php echo $property['status_name']; ?> </span> <span class="badge bg-primary position-absolute bottom-0 start-0 m-3"> <?php echo formatAmount($property['price']); ?> </span> </div> <div class="card-body"> <h5 class="card-title"> <a href="property-details.php?slug=<?php echo $property['slug']; ?>" class="text-decoration-none text-dark"> <?php echo htmlspecialchars($property['title']); ?> </a> </h5> <p class="card-text text-muted small"> <i class="fas fa-map-marker-alt me-1"></i> <?php echo $property['city'] . ', ' . $property['state']; ?> </p> <div class="property-features d-flex justify-content-between text-center mt-3"> <div> <i class="fas fa-bed text-primary"></i> <span class="d-block small"><?php echo $property['bedrooms'] ?? 0; ?> Beds</span> </div> <div> <i class="fas fa-bath text-primary"></i> <span class="d-block small"><?php echo $property['bathrooms'] ?? 0; ?> Baths</span> </div> <div> <i class="fas fa-
I'll continue with the rest of the code for the Real Estate Listing Website.
Main Landing Page (Continued)
File: index.php (continued)
<div>
<i class="fas fa-arrows-alt text-primary"></i>
<span class="d-block small"><?php echo number_format($property['sqft'] ?? 0); ?> sqft</span>
</div>
</div>
</div>
<div class="card-footer bg-white border-0 pb-3">
<a href="property-details.php?slug=<?php echo $property['slug']; ?>" class="btn btn-outline-primary w-100">
View Details <i class="fas fa-arrow-right ms-2"></i>
</a>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</section>
<!-- How It Works -->
<section class="py-5 bg-light">
<div class="container">
<h2 class="text-center fw-bold mb-5">How It Works</h2>
<div class="row g-4">
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="step-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-search fa-2x"></i>
</div>
<h4>1. Search Properties</h4>
<p class="text-muted">Browse thousands of properties using our advanced search filters.</p>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="step-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-calendar-check fa-2x"></i>
</div>
<h4>2. Schedule Viewings</h4>
<p class="text-muted">Book appointments to view properties that interest you.</p>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="step-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-handshake fa-2x"></i>
</div>
<h4>3. Make an Offer</h4>
<p class="text-muted">Connect with agents and make offers on your dream property.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Recent Properties -->
<section class="py-5">
<div class="container">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold">Recently Added</h2>
<a href="properties.php" class="btn btn-outline-primary">
View All <i class="fas fa-arrow-right ms-2"></i>
</a>
</div>
<div class="row g-4">
<?php foreach ($recentProperties as $property): ?>
<div class="col-lg-4 col-md-6">
<div class="card property-card h-100 border-0 shadow-sm">
<div class="position-relative">
<img src="uploads/properties/<?php echo $property['primary_image'] ?? 'default.jpg'; ?>"
class="card-img-top" alt="<?php echo htmlspecialchars($property['title']); ?>">
<span class="badge bg-<?php echo $property['status_color']; ?> position-absolute top-0 end-0 m-3">
<?php echo $property['status_name']; ?>
</span>
<span class="badge bg-primary position-absolute bottom-0 start-0 m-3">
<?php echo formatAmount($property['price']); ?>
</span>
</div>
<div class="card-body">
<h5 class="card-title">
<a href="property-details.php?slug=<?php echo $property['slug']; ?>"
class="text-decoration-none text-dark">
<?php echo htmlspecialchars($property['title']); ?>
</a>
</h5>
<p class="card-text text-muted small">
<i class="fas fa-map-marker-alt me-1"></i>
<?php echo $property['city'] . ', ' . $property['state']; ?>
</p>
<div class="property-features d-flex justify-content-between text-center mt-3">
<div>
<i class="fas fa-bed text-primary"></i>
<span class="d-block small"><?php echo $property['bedrooms'] ?? 0; ?> Beds</span>
</div>
<div>
<i class="fas fa-bath text-primary"></i>
<span class="d-block small"><?php echo $property['bathrooms'] ?? 0; ?> Baths</span>
</div>
<div>
<i class="fas fa-arrows-alt text-primary"></i>
<span class="d-block small"><?php echo number_format($property['sqft'] ?? 0); ?> sqft</span>
</div>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</section>
<!-- Why Choose Us -->
<section class="py-5 bg-primary text-white">
<div class="container">
<h2 class="text-center fw-bold mb-5">Why Choose Us</h2>
<div class="row g-4">
<div class="col-md-3">
<div class="text-center">
<i class="fas fa-building fa-3x mb-3"></i>
<h5>10,000+ Properties</h5>
<p class="text-white-50">Wide selection of properties across the country</p>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<i class="fas fa-user-tie fa-3x mb-3"></i>
<h5>500+ Agents</h5>
<p class="text-white-50">Professional and experienced real estate agents</p>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<i class="fas fa-clock fa-3x mb-3"></i>
<h5>24/7 Support</h5>
<p class="text-white-50">Round-the-clock customer support</p>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<i class="fas fa-shield-alt fa-3x mb-3"></i>
<h5>Secure Transactions</h5>
<p class="text-white-50">Safe and secure payment processing</p>
</div>
</div>
</div>
</div>
</section>
<!-- Testimonials -->
<section class="py-5">
<div class="container">
<h2 class="text-center fw-bold mb-5">What Our Clients Say</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body p-4">
<div class="mb-3 text-warning">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
</div>
<p class="card-text">"Found my dream home through this platform. The search filters made it easy to find exactly what I was looking for."</p>
<div class="d-flex align-items-center">
<img src="assets/images/avatars/user1.jpg" class="rounded-circle me-3" width="50" height="50" alt="User">
<div>
<h6 class="mb-0">John Smith</h6>
<small class="text-muted">Home Buyer</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body p-4">
<div class="mb-3 text-warning">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
</div>
<p class="card-text">"As a real estate agent, this platform has helped me reach more clients and manage my listings efficiently."</p>
<div class="d-flex align-items-center">
<img src="assets/images/avatars/user2.jpg" class="rounded-circle me-3" width="50" height="50" alt="User">
<div>
<h6 class="mb-0">Sarah Johnson</h6>
<small class="text-muted">Real Estate Agent</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body p-4">
<div class="mb-3 text-warning">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
</div>
<p class="card-text">"Sold my property within weeks of listing. The agent assigned was professional and handled everything smoothly."</p>
<div class="d-flex align-items-center">
<img src="assets/images/avatars/user3.jpg" class="rounded-circle me-3" width="50" height="50" alt="User">
<div>
<h6 class="mb-0">Michael Brown</h6>
<small class="text-muted">Property Seller</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Call to Action -->
<section class="py-5 bg-light">
<div class="container text-center">
<h2 class="fw-bold mb-4">Ready to Find Your Dream Home?</h2>
<p class="lead mb-4">Join thousands of satisfied customers who found their perfect property with us.</p>
<?php if (!$auth->isLoggedIn()): ?>
<a href="register.php" class="btn btn-primary btn-lg px-5 me-2">Sign Up Now</a>
<a href="contact.php" class="btn btn-outline-primary btn-lg px-5">Contact Us</a>
<?php else: ?>
<a href="properties.php" class="btn btn-primary btn-lg px-5">Browse Properties</a>
<?php endif; ?>
</div>
</section>
<!-- Footer -->
<footer class="bg-dark text-white py-5">
<div class="container">
<div class="row">
<div class="col-md-4 mb-4">
<h5><i class="fas fa-home me-2"></i><?php echo SITE_NAME; ?></h5>
<p class="text-white-50">Your trusted partner in real estate. Find, buy, or rent properties with ease.</p>
<div class="social-links">
<a href="<?php echo FACEBOOK_URL; ?>" class="text-white me-2" target="_blank">
<i class="fab fa-facebook fa-lg"></i>
</a>
<a href="<?php echo TWITTER_URL; ?>" class="text-white me-2" target="_blank">
<i class="fab fa-twitter fa-lg"></i>
</a>
<a href="<?php echo INSTAGRAM_URL; ?>" class="text-white me-2" target="_blank">
<i class="fab fa-instagram fa-lg"></i>
</a>
<a href="<?php echo LINKEDIN_URL; ?>" class="text-white me-2" target="_blank">
<i class="fab fa-linkedin fa-lg"></i>
</a>
</div>
</div>
<div class="col-md-2 mb-4">
<h6>Quick Links</h6>
<ul class="list-unstyled">
<li><a href="index.php" class="text-white-50">Home</a></li>
<li><a href="properties.php" class="text-white-50">Properties</a></li>
<li><a href="agents.php" class="text-white-50">Agents</a></li>
<li><a href="about.php" class="text-white-50">About Us</a></li>
<li><a href="contact.php" class="text-white-50">Contact</a></li>
</ul>
</div>
<div class="col-md-3 mb-4">
<h6>Property Types</h6>
<ul class="list-unstyled">
<?php foreach (array_slice($propertyTypes, 0, 5) as $type): ?>
<li>
<a href="properties.php?type=<?php echo $type['id']; ?>" class="text-white-50">
<?php echo $type['name']; ?>
</a>
</li>
<?php endforeach; ?>
<li><a href="properties.php" class="text-white-50">All Types</a></li>
</ul>
</div>
<div class="col-md-3 mb-4">
<h6>Contact Info</h6>
<ul class="list-unstyled text-white-50">
<li><i class="fas fa-map-marker-alt me-2"></i><?php echo ADDRESS; ?></li>
<li><i class="fas fa-phone me-2"></i><?php echo CONTACT_PHONE; ?></li>
<li><i class="fas fa-envelope me-2"></i><?php echo CONTACT_EMAIL; ?></li>
</ul>
</div>
</div>
<hr class="border-secondary">
<div class="row">
<div class="col-12 text-center">
<p class="text-white-50 mb-0">
© <?php echo date('Y'); ?> <?php echo SITE_NAME; ?>. All rights reserved.
</p>
</div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js"></script>
<script src="assets/js/main.js"></script>
<style>
.navbar {
padding: 1rem 0;
}
.hero-section {
margin-top: 76px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.search-box {
max-width: 900px;
margin: 0 auto;
}
.property-card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
overflow: hidden;
}
.property-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0,0,0,0.15) !important;
}
.property-card img {
height: 250px;
object-fit: cover;
transition: transform 0.3s ease;
}
.property-card:hover img {
transform: scale(1.05);
}
.step-icon {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
}
.badge {
font-size: 0.9rem;
padding: 0.5rem 1rem;
}
.property-features div {
flex: 1;
}
.property-features i {
font-size: 1.2rem;
}
@media (max-width: 768px) {
.hero-section {
margin-top: 68px;
}
.property-card img {
height: 200px;
}
}
</style>
</body>
</html>
Properties Listing Page
File: properties.php
<?php require_once 'includes/config.php'; // Initialize classes $property = new Property(); // Get filter parameters $filters = [ 'property_type' => $_GET['type'] ?? null, 'status' => $_GET['status'] ?? null, 'city' => $_GET['city'] ?? null, 'state' => $_GET['state'] ?? null, 'min_price' => $_GET['min_price'] ?? null, 'max_price' => $_GET['max_price'] ?? null, 'min_bedrooms' => $_GET['beds'] ?? null, 'min_bathrooms' => $_GET['baths'] ?? null, 'search' => $_GET['search'] ?? null, 'featured' => isset($_GET['featured']), 'page' => $_GET['page'] ?? 1 ]; // Get properties $properties = $property->getProperties($filters); $totalCount = $property->getCount($filters); $totalPages = ceil($totalCount / PROPERTIES_PER_PAGE); // Get filter options $propertyTypes = $property->getPropertyTypes(); $statuses = $property->getStatuses(); $cities = $property->getCities(); $priceRanges = $property->getPriceRanges(); ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Properties for Sale & Rent - <?php echo SITE_NAME; ?></title> <!-- Bootstrap 5 CSS --> <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> <!-- Font Awesome --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <!-- Range Slider CSS --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/nouislider.min.css"> <!-- Custom CSS --> <link rel="stylesheet" href="assets/css/style.css"> </head> <body> <!-- Navigation (same as index.php) --> <!-- ... navigation code ... --> <div class="container mt-5 pt-5"> <div class="row"> <!-- Sidebar Filters --> <div class="col-lg-3 mb-4"> <div class="card border-0 shadow-sm sticky-top" style="top: 100px;"> <div class="card-header bg-white"> <h5 class="mb-0"> <i class="fas fa-filter me-2 text-primary"></i> Filter Properties </h5> </div> <div class="card-body"> <form action="" method="GET" id="filterForm"> <!-- Search --> <div class="mb-3"> <label class="form-label fw-semibold">Search</label> <input type="text" class="form-control" name="search" value="<?php echo htmlspecialchars($_GET['search'] ?? ''); ?>" placeholder="Enter location, address..."> </div> <!-- Property Type --> <div class="mb-3"> <label class="form-label fw-semibold">Property Type</label> <select class="form-select" name="type"> <option value="">All Types</option> <?php foreach ($propertyTypes as $type): ?> <option value="<?php echo $type['id']; ?>" <?php echo ($_GET['type'] ?? '') == $type['id'] ? 'selected' : ''; ?>> <?php echo $type['name']; ?> </option> <?php endforeach; ?> </select> </div> <!-- Status --> <div class="mb-3"> <label class="form-label fw-semibold">Status</label> <select class="form-select" name="status"> <option value="">All Status</option> <?php foreach ($statuses as $status): ?> <option value="<?php echo $status['id']; ?>" <?php echo ($_GET['status'] ?? '') == $status['id'] ? 'selected' : ''; ?>> <?php echo $status['name']; ?> </option> <?php endforeach; ?> </select> </div> <!-- Price Range --> <div class="mb-3"> <label class="form-label fw-semibold">Price Range</label> <div class="row g-2"> <div class="col-6"> <select class="form-select" name="min_price"> <option value="">Min Price</option> <?php foreach ($priceRanges as $range): ?> <?php if ($range['min'] !== null): ?> <option value="<?php echo $range['min']; ?>" <?php echo ($_GET['min_price'] ?? '') == $range['min'] ? 'selected' : ''; ?>> <?php echo formatAmount($range['min']); ?>+ </option> <?php endif; ?> <?php endforeach; ?> </select> </div> <div class="col-6"> <select class="form-select" name="max_price"> <option value="">Max Price</option> <?php foreach ($priceRanges as $range): ?> <?php if ($range['max'] !== null): ?> <option value="<?php echo $range['max']; ?>" <?php echo ($_GET['max_price'] ?? '') == $range['max'] ? 'selected' : ''; ?>> Up to <?php echo formatAmount($range['max']); ?> </option> <?php endif; ?> <?php endforeach; ?> <option value="99999999" <?php echo ($_GET['max_price'] ?? '') == '99999999' ? 'selected' : ''; ?>> $2M+ </option> </select> </div> </div> </div> <!-- Beds & Baths --> <div class="row mb-3"> <div class="col-6"> <label class="form-label fw-semibold">Beds</label> <select class="form-select" name="beds"> <option value="">Any</option> <?php for ($i = 1; $i <= 5; $i++): ?> <option value="<?php echo $i; ?>" <?php echo ($_GET['beds'] ?? '') == $i ? 'selected' : ''; ?>> <?php echo $i; ?>+ Beds </option> <?php endfor; ?> </select> </div> <div class="col-6"> <label class="form-label fw-semibold">Baths</label> <select class="form-select" name="baths"> <option value="">Any</option> <?php for ($i = 1; $i <= 5; $i++): ?> <option value="<?php echo $i; ?>" <?php echo ($_GET['baths'] ?? '') == $i ? 'selected' : ''; ?>> <?php echo $i; ?>+ Baths </option> <?php endfor; ?> </select> </div> </div> <!-- City --> <div class="mb-3"> <label class="form-label fw-semibold">City</label> <select class="form-select" name="city"> <option value="">All Cities</option> <?php foreach ($cities as $city): ?> <option value="<?php echo $city['city']; ?>" <?php echo ($_GET['city'] ?? '') == $city['city'] ? 'selected' : ''; ?>> <?php echo $city['city']; ?> (<?php echo $city['property_count']; ?>) </option> <?php endforeach; ?> </select> </div> <!-- Featured Only --> <div class="mb-3 form-check"> <input type="checkbox" class="form-check-input" name="featured" id="featured" value="1" <?php echo isset($_GET['featured']) ? 'checked' : ''; ?>> <label class="form-check-label" for="featured"> <i class="fas fa-star text-warning me-1"></i> Featured Properties Only </label> </div> <button type="submit" class="btn btn-primary w-100 mb-2"> <i class="fas fa-search me-2"></i>Apply Filters </button> <a href="properties.php" class="btn btn-outline-secondary w-100"> <i class="fas fa-undo me-2"></i>Clear Filters </a> </form> </div> </div> </div> <!-- Properties Grid --> <div class="col-lg-9"> <!-- Results Header --> <div class="d-flex justify-content-between align-items-center mb-4"> <h4 class="fw-bold mb-0"> <?php echo number_format($totalCount); ?> Properties Found </h4> <div class="d-flex align-items-center"> <label class="me-2 text-muted">Sort by:</label> <select class="form-select" style="width: auto;" onchange="window.location.href=this.value"> <option value="?sort=date_desc" <?php echo ($_GET['sort'] ?? '') == 'date_desc' ? 'selected' : ''; ?>> Newest First </option> <option value="?sort=date_asc" <?php echo ($_GET['sort'] ?? '') == 'date_asc' ? 'selected' : ''; ?>> Oldest First </option> <option value="?sort=price_desc" <?php echo ($_GET['sort'] ?? '') == 'price_desc' ? 'selected' : ''; ?>> Price: High to Low </option> <option value="?sort=price_asc" <?php echo ($_GET['sort'] ?? '') == 'price_asc' ? 'selected' : ''; ?>> Price: Low to High </option> </select> </div> </div> <!-- Properties Grid --> <?php if (empty($properties)): ?> <div class="text-center py-5"> <i class="fas fa-home fa-4x text-muted mb-3"></i> <h5>No Properties Found</h5> <p class="text-muted">Try adjusting your filters or search criteria.</p> </div> <?php else: ?> <div class="row g-4"> <?php foreach ($properties as $property): ?> <div class="col-md-6 col-xl-4"> <div class="card property-card h-100 border-0 shadow-sm"> <div class="position-relative"> <img src="uploads/properties/<?php echo $property['primary_image'] ?? 'default.jpg'; ?>" class="card-img-top" alt="<?php echo htmlspecialchars($property['title']); ?>"> <?php if ($property['featured']): ?> <span class="badge bg-warning position-absolute top-0 start-0 m-3"> <i class="fas fa-star me-1"></i>Featured </span> <?php endif; ?> <span class="badge bg-<?php echo $property['status_color']; ?> position-absolute top-0 end-0 m-3"> <?php echo $property['status_name']; ?> </span> <span class="badge bg-primary position-absolute bottom-0 start-0 m-3"> <?php echo formatAmount($property['price']); ?> </span> </div> <div class="card-body"> <h6 class="card-title"> <a href="property-details.php?slug=<?php echo $property['slug']; ?>" class="text-decoration-none text-dark"> <?php echo htmlspecialchars($property['title']); ?> </a> </h6> <p class="card-text text-muted small"> <i class="fas fa-map-marker-alt me-1"></i> <?php echo $property['city'] . ', ' . $property['state']; ?> </p> <div class="property-features d-flex justify-content-between text-center mt-3"> <div> <i class="fas fa-bed text-primary"></i> <span class="d-block small"><?php echo $property['bedrooms'] ?? 0; ?> Beds</span> </div> <div> <i class="fas fa-bath text-primary"></i> <span class="d-block small"><?php echo $property['bathrooms'] ?? 0; ?> Baths</span> </div> <div> <i class="fas fa-arrows-alt text-primary"></i> <span class="d-block small"><?php echo number_format($property['sqft'] ?? 0); ?> sqft</span> </div> </div> </div> <div class="card-footer bg-white border-0 pb-3"> <a href="property-details.php?slug=<?php echo $property['slug']; ?>" class="btn btn-outline-primary w-100"> View Details <i class="fas fa-arrow-right ms-2"></i> </a> </div> </div> </div> <?php endforeach; ?> </div> <!-- Pagination --> <?php if ($totalPages > 1): ?> <div class="mt-5"> <?php $url = 'properties.php?' . http_build_query(array_merge($_GET, ['page' => ''])); echo paginate($filters['page'], $totalPages, $url); ?> </div> <?php endif; ?> <?php endif; ?> </div> </div> </div> <!-- Footer (same as index.php) --> <!-- ... footer code ... --> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="assets/js/main.js"></script> <style> .property-card { transition: transform 0.3s ease, box-shadow 0.3s ease; overflow: hidden; } .property-card:hover { transform: translateY(-5px); box-shadow: 0 15px 40px rgba(0,0,0,0.15) !important; } .property-card img { height: 200px; object-fit: cover; transition: transform 0.3s ease; } .property-card:hover img { transform: scale(1.05); } .sticky-top { top: 100px; z-index: 1020; } .badge { font-size: 0.8rem; padding: 0.4rem 0.8rem; } .property-features div { flex: 1; } .property-features i { font-size: 1rem; } @media (max-width: 991px) { .sticky-top { position: relative !important; top: 0; } } </style> </body> </html>
Property Details Page
File: property-details.php
<?php
require_once 'includes/config.php';
// Initialize classes
$property = new Property();
$inquiry = new Inquiry();
$favorite = new Favorite();
// Get property slug from URL
$slug = $_GET['slug'] ?? '';
if (empty($slug)) {
header('Location: properties.php');
exit();
}
// Get property details
$propertyDetails = $property->getBySlug($slug);
if (!$propertyDetails) {
header('Location: properties.php');
exit();
}
// Increment view count
$property->incrementViews($propertyDetails['id']);
// Get property images
$images = $property->getImages($propertyDetails['id']);
// Get property amenities
$amenities = $property->getAmenities($propertyDetails['id']);
// Get similar properties
$similarProperties = $property->getSimilar($propertyDetails['id']);
// Check if property is favorited by current user
$isFavorite = false;
if ($auth->isLoggedIn()) {
$isFavorite = $favorite->isFavorite($_SESSION['user_id'], $propertyDetails['id']);
}
// Handle inquiry form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if ($_POST['action'] === 'inquiry') {
$result = $inquiry->create([
'property_id' => $propertyDetails['id'],
'agent_id' => $propertyDetails['agent_id'],
'user_id' => $_SESSION['user_id'] ?? null,
'name' => $_POST['name'],
'email' => $_POST['email'],
'phone' => $_POST['phone'] ?? null,
'message' => $_POST['message'],
'type' => $_POST['type'] ?? 'general'
]);
if ($result['success']) {
$_SESSION['success'] = 'Your inquiry has been sent successfully!';
} else {
$_SESSION['error'] = $result['error'];
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($propertyDetails['title']); ?> - <?php echo SITE_NAME; ?></title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Lightbox CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.4/css/lightbox.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<!-- Navigation (same as index.php) -->
<!-- ... navigation code ... -->
<div class="container mt-5 pt-5">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="index.php">Home</a></li>
<li class="breadcrumb-item"><a href="properties.php">Properties</a></li>
<li class="breadcrumb-item active" aria-current="page">
<?php echo htmlspecialchars($propertyDetails['title']); ?>
</li>
</ol>
</nav>
<!-- Property Title -->
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<h1 class="fw-bold"><?php echo htmlspecialchars($propertyDetails['title']); ?></h1>
<p class="text-muted">
<i class="fas fa-map-marker-alt me-2 text-primary"></i>
<?php echo $propertyDetails['address']; ?>,
<?php echo $propertyDetails['city'] . ', ' . $propertyDetails['state'] . ' ' . $propertyDetails['zipcode']; ?>
</p>
</div>
<div class="text-end">
<h2 class="text-primary fw-bold mb-2"><?php echo formatAmount($propertyDetails['price']); ?></h2>
<span class="badge bg-<?php echo $propertyDetails['status_color']; ?> p-2">
<?php echo $propertyDetails['status_name']; ?>
</span>
<?php if ($auth->isLoggedIn() && $_SESSION['user_role'] === 'buyer'): ?>
<button class="btn btn-outline-danger ms-2" onclick="toggleFavorite(<?php echo $propertyDetails['id']; ?>)">
<i class="fas fa-heart <?php echo $isFavorite ? 'text-danger' : 'text-muted'; ?>"></i>
</button>
<?php endif; ?>
</div>
</div>
<div class="row">
<!-- Main Content -->
<div class="col-lg-8">
<!-- Image Gallery -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-0">
<?php if (!empty($images)): ?>
<div class="row g-1">
<div class="col-md-8">
<a href="uploads/properties/<?php echo $images[0]['image_path']; ?>"
data-lightbox="property"
data-title="<?php echo htmlspecialchars($propertyDetails['title']); ?>">
<img src="uploads/properties/<?php echo $images[0]['image_path']; ?>"
class="img-fluid w-100" style="height: 400px; object-fit: cover;"
alt="<?php echo htmlspecialchars($propertyDetails['title']); ?>">
</a>
</div>
<div class="col-md-4">
<div class="row g-1">
<?php for ($i = 1; $i < min(4, count($images)); $i++): ?>
<div class="col-6">
<a href="uploads/properties/<?php echo $images[$i]['image_path']; ?>"
data-lightbox="property"
data-title="<?php echo htmlspecialchars($propertyDetails['title']); ?>">
<img src="uploads/properties/<?php echo $images[$i]['image_path']; ?>"
class="img-fluid w-100" style="height: 198px; object-fit: cover;"
alt="<?php echo htmlspecialchars($propertyDetails['title']); ?>">
</a>
</div>
<?php endfor; ?>
<?php if (count($images) > 4): ?>
<div class="col-12 mt-1">
<a href="uploads/properties/<?php echo $images[4]['image_path']; ?>"
data-lightbox="property"
class="btn btn-primary w-100">
+<?php echo count($images) - 4; ?> More Photos
</a>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php else: ?>
<img src="uploads/properties/default.jpg" class="img-fluid w-100"
style="height: 400px; object-fit: cover;"
alt="<?php echo htmlspecialchars($propertyDetails['title']); ?>">
<?php endif; ?>
</div>
</div>
<!-- Key Features -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title mb-4">Key Features</h5>
<div class="row g-4">
<div class="col-md-3 col-6">
<div class="text-center">
<i class="fas fa-bed fa-2x text-primary mb-2"></i>
<h6 class="mb-1"><?php echo $propertyDetails['bedrooms'] ?? 0; ?></h6>
<small class="text-muted">Bedrooms</small>
</div>
</div>
<div class="col-md-3 col-6">
<div class="text-center">
<i class="fas fa-bath fa-2x text-primary mb-2"></i>
<h6 class="mb-1"><?php echo $propertyDetails['bathrooms'] ?? 0; ?></h6>
<small class="text-muted">Bathrooms</small>
</div>
</div>
<div class="col-md-3 col-6">
<div class="text-center">
<i class="fas fa-arrows-alt fa-2x text-primary mb-2"></i>
<h6 class="mb-1"><?php echo number_format($propertyDetails['sqft'] ?? 0); ?></h6>
<small class="text-muted">Square Feet</small>
</div>
</div>
<div class="col-md-3 col-6">
<div class="text-center">
<i class="fas fa-calendar fa-2x text-primary mb-2"></i>
<h6 class="mb-1"><?php echo $propertyDetails['year_built'] ?? 'N/A'; ?></h6>
<small class="text-muted">Year Built</small>
</div>
</div>
<div class="col-md-3 col-6">
<div class="text-center">
<i class="fas fa-car fa-2x text-primary mb-2"></i>
<h6 class="mb-1"><?php echo $propertyDetails['parking_spaces'] ?? 0; ?></h6>
<small class="text-muted">Parking</small>
</div>
</div>
<div class="col-md-3 col-6">
<div class="text-center">
<i class="fas fa-tree fa-2x text-primary mb-2"></i>
<h6 class="mb-1"><?php echo number_format($propertyDetails['lot_size'] ?? 0); ?></h6>
<small class="text-muted">Lot Size (sqft)</small>
</div>
</div>
<div class="col-md-3 col-6">
<div class="text-center">
<i class="fas fa-layer-group fa-2x text-primary mb-2"></i>
<h6 class="mb-1"><?php echo $propertyDetails['property_type_name']; ?></h6>
<small class="text-muted">Property Type</small>
</div>
</div>
<div class="col-md-3 col-6">
<div class="text-center">
<i class="fas fa-eye fa-2x text-primary mb-2"></i>
<h6 class="mb-1"><?php echo number_format($propertyDetails['views'] ?? 0); ?></h6>
<small class="text-muted">Views</small>
</div>
</div>
</div>
</div>
</div>
<!-- Description -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title mb-4">Description</h5>
<p class="card-text"><?php echo nl2br(htmlspecialchars($propertyDetails['description'])); ?></p>
</div>
</div>
<!-- Amenities -->
<?php if (!empty($amenities)): ?>
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title mb-4">Amenities & Features</h5>
<div class="row g-3">
<?php foreach ($amenities as $amenity): ?>
<div class="col-md-4 col-6">
<div class="d-flex align-items-center">
<i class="fas fa-check-circle text-success me-2"></i>
<span><?php echo htmlspecialchars($amenity['name']); ?></span>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
<!-- Map -->
<?php if (!empty(MAP_API_KEY) && !empty($propertyDetails['latitude']) && !empty($propertyDetails['longitude'])): ?>
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title mb-4">Location</h5>
<div id="map" style="height: 400px;"></div>
</div>
</div>
<?php endif; ?>
<!-- Similar Properties -->
<?php if (!empty($similarProperties)): ?>
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title mb-4">Similar Properties</h5>
<div class="row g-4">
<?php foreach ($similarProperties as $similar): ?>
<div class="col-md-4">
<div class="card property-card h-100 border-0">
<img src="uploads/properties/<?php echo $similar['primary_image'] ?? 'default.jpg'; ?>"
class="card-img-top" style="height: 150px; object-fit: cover;"
alt="<?php echo htmlspecialchars($similar['title']); ?>">
<div class="card-body p-3">
<h6 class="card-title small">
<a href="property-details.php?slug=<?php echo $similar['slug']; ?>"
class="text-decoration-none text-dark">
<?php echo htmlspecialchars($similar['title']); ?>
</a>
</h6>
<div class="d-flex justify-content-between align-items-center">
<span class="text-primary fw-bold">
<?php echo formatAmount($similar['price']); ?>
</span>
<span class="badge bg-<?php echo $similar['status_color']; ?>">
<?php echo $similar['status_name']; ?>
</span>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Agent Info -->
<?php if ($propertyDetails['agent_id']): ?>
<div class="card border-0 shadow-sm mb-4 sticky-top" style="top: 100px;">
<div class="card-body text-center">
<img src="uploads/agents/<?php echo $propertyDetails['agent_image'] ?? 'default.jpg'; ?>"
class="rounded-circle mb-3" width="100" height="100"
style="object-fit: cover;"
alt="<?php echo htmlspecialchars($propertyDetails['agent_first_name'] . ' ' . $propertyDetails['agent_last_name']); ?>">
<h5 class="mb-1"><?php echo htmlspecialchars($propertyDetails['agent_first_name'] . ' ' . $propertyDetails['agent_last_name']); ?></h5>
<p class="text-muted small mb-3"><?php echo htmlspecialchars($propertyDetails['business_name']); ?></p>
<div class="mb-3">
<div class="rating mb-1">
<?php
$agentRating = $propertyDetails['agent_rating'] ?? 0;
echo getRatingStars($agentRating);
?>
</div>
<small class="text-muted"><?php echo $propertyDetails['agent_reviews'] ?? 0; ?> reviews</small>
</div>
<div class="d-grid gap-2">
<a href="agent-details.php?id=<?php echo $propertyDetails['agent_id']; ?>"
class="btn btn-outline-primary">
<i class="fas fa-user me-2"></i>View Profile
</a>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#inquiryModal">
<i class="fas fa-envelope me-2"></i>Contact Agent
</button>
</div>
</div>
</div>
<?php endif; ?>
<!-- Mortgage Calculator -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title mb-3">Mortgage Calculator</h5>
<form id="mortgageForm">
<div class="mb-3">
<label class="form-label">Loan Amount</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" id="loanAmount"
value="<?php echo $propertyDetails['price'] * 0.8; ?>">
</div>
</div>
<div class="mb-3">
<label class="form-label">Down Payment</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" id="downPayment"
value="<?php echo $propertyDetails['price'] * 0.2; ?>">
</div>
</div>
<div class="mb-3">
<label class="form-label">Interest Rate (%)</label>
<input type="number" class="form-control" id="interestRate" value="4.5" step="0.1">
</div>
<div class="mb-3">
<label class="form-label">Loan Term (years)</label>
<select class="form-select" id="loanTerm">
<option value="15">15 years</option>
<option value="30" selected>30 years</option>
</select>
</div>
</form>
<div class="mt-3 p-3 bg-light rounded">
<div class="d-flex justify-content-between mb-2">
<span>Monthly Payment:</span>
<span class="fw-bold text-primary" id="monthlyPayment">$0</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span>Total Interest:</span>
<span class="fw-bold" id="totalInterest">$0</span>
</div>
<div class="d-flex justify-content-between">
<span>Total Payment:</span>
<span class="fw-bold" id="totalPayment">$0</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Inquiry Modal -->
<div class="modal fade" id="inquiryModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Contact Agent</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="inquiry">
<?php if (!$auth->isLoggedIn()): ?>
<div class="mb-3">
<label class="form-label">Name *</label>
<input type="text" class="form-control" name="name" required>
</div>
<div class="mb-3">
<label class="form-label">Email *</label>
<input type="email" class="form-control" name="email" required>
</div>
<div class="mb-3">
<label class="form-label">Phone</label>
<input type="tel" class="form-control" name="phone">
</div>
<?php endif; ?>
<div class="mb-3">
<label class="form-label">Message *</label>
<textarea class="form-control" name="message" rows="4" required
placeholder="I'm interested in this property. Please contact me with more information."></textarea>
</div>
<div class="mb-3">
<label class="form-label">Inquiry Type</label>
<select class="form-select" name="type">
<option value="general">General Inquiry</option>
<option value="viewing">Schedule a Viewing</option>
<option value="offer">Make an Offer</option>
<option value="question">Question</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Send Message</button>
</div>
</form>
</div>
</div>
</div>
<!-- Footer (same as index.php) -->
<!-- ... footer code ... -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.4/js/lightbox.min.js"></script>
<?php if (!empty(MAP_API_KEY)): ?>
<script src="https://maps.googleapis.com/maps/api/js?key=<?php echo MAP_API_KEY; ?>"></script>
<script>
function initMap() {
const propertyLocation = {
lat: <?php echo $propertyDetails['latitude']; ?>,
lng: <?php echo $propertyDetails['longitude']; ?>
};
const map = new google.maps.Map(document.getElementById('map'), {
zoom: 15,
center: propertyLocation
});
new google.maps.Marker({
position: propertyLocation,
map: map,
title: '<?php echo addslashes($propertyDetails['title']); ?>'
});
}
initMap();
</script>
<?php endif; ?>
<script>
// Mortgage Calculator
function calculateMortgage() {
const price = <?php echo $propertyDetails['price']; ?>;
const loanAmount = parseFloat(document.getElementById('loanAmount').value) || 0;
const downPayment = parseFloat(document.getElementById('downPayment').value) || 0;
const interestRate = parseFloat(document.getElementById('interestRate').value) || 0;
const loanTerm = parseInt(document.getElementById('loanTerm').value) || 30;
const principal = loanAmount - downPayment;
const monthlyRate = interestRate / 100 / 12;
const totalPayments = loanTerm * 12;
let monthlyPayment = 0;
if (monthlyRate > 0) {
monthlyPayment = principal * monthlyRate * Math.pow(1 + monthlyRate, totalPayments) /
(Math.pow(1 + monthlyRate, totalPayments) - 1);
} else {
monthlyPayment = principal / totalPayments;
}
const totalPayment = monthlyPayment * totalPayments;
const totalInterest = totalPayment - principal;
document.getElementById('monthlyPayment').textContent = '$' + monthlyPayment.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
document.getElementById('totalInterest').textContent = '$' + totalInterest.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
document.getElementById('totalPayment').textContent = '$' + totalPayment.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
document.getElementById('loanAmount').addEventListener('input', calculateMortgage);
document.getElementById('downPayment').addEventListener('input', calculateMortgage);
document.getElementById('interestRate').addEventListener('input', calculateMortgage);
document.getElementById('loanTerm').addEventListener('change', calculateMortgage);
calculateMortgage();
// Toggle Favorite
function toggleFavorite(propertyId) {
<?php if (!$auth->isLoggedIn()): ?>
window.location.href = 'login.php';
return;
<?php endif; ?>
$.post('api/favorite.php', {
action: 'toggle',
property_id: propertyId
}, function(response) {
if (response.success) {
const icon = $('.btn-outline-danger i');
if (response.favorited) {
icon.removeClass('text-muted').addClass('text-danger');
} else {
icon.removeClass('text-danger').addClass('text-muted');
}
}
});
}
</script>
<style>
.property-card {
transition: transform 0.3s ease;
}
.property-card:hover {
transform: translateY(-3px);
}
.sticky-top {
top: 100px;
z-index: 1020;
}
.rating i {
font-size: 0.9rem;
}
</style>
</body>
</html>
Login Page
File: login.php
<?php
require_once 'includes/config.php';
// Redirect if already logged in
if ($auth->isLoggedIn()) {
$role = $_SESSION['user_role'];
if ($role === 'admin') {
redirect('/admin/dashboard.php');
} elseif ($role === 'agent') {
redirect('/agent/dashboard.php');
} elseif ($role === 'seller') {
redirect('/seller/dashboard.php');
} else {
redirect('/buyer/dashboard.php');
}
}
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = sanitize($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$remember = isset($_POST['remember']);
if (empty($email) || empty($password)) {
$error = 'Please enter email and password';
} else {
$result = $auth->login($email, $password, $remember);
if ($result['success']) {
$user = $result['user'];
// Redirect based on role
if ($user['role'] === 'admin') {
redirect('/admin/dashboard.php');
} elseif ($user['role'] === 'agent') {
redirect('/agent/dashboard.php');
} elseif ($user['role'] === 'seller') {
redirect('/seller/dashboard.php');
} else {
redirect('/buyer/dashboard.php');
}
} else {
$error = $result['error'];
}
}
}
// Check for session messages
if (isset($_SESSION['success'])) {
$success = $_SESSION['success'];
unset($_SESSION['success']);
}
if (isset($_SESSION['error'])) {
$error = $_SESSION['error'];
unset($_SESSION['error']);
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - <?php echo SITE_NAME; ?></title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="bg-light">
<div class="container">
<div class="row justify-content-center align-items-center min-vh-100">
<div class="col-md-6 col-lg-5">
<div class="card shadow-lg border-0 rounded-4">
<div class="card-header bg-primary text-white text-center py-4 rounded-top-4">
<h3 class="mb-0">
<i class="fas fa-home me-2"></i>
<?php echo SITE_NAME; ?>
</h3>
<p class="mb-0 text-white-50">Welcome back! Please login to your account.</p>
</div>
<div class="card-body p-5">
<?php if ($error): ?>
<div class="alert alert-danger alert-dismissible fade show">
<i class="fas fa-exclamation-circle me-2"></i>
<?php echo $error; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success alert-dismissible fade show">
<i class="fas fa-check-circle me-2"></i>
<?php echo $success; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<form method="POST" action="" onsubmit="return validateLogin()">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<div class="mb-4">
<label for="email" class="form-label">
<i class="fas fa-envelope me-2"></i>Email Address
</label>
<div class="input-group">
<span class="input-group-text bg-light">
<i class="fas fa-envelope text-primary"></i>
</span>
<input type="email" class="form-control" id="email" name="email"
placeholder="Enter your email" required>
</div>
</div>
<div class="mb-4">
<label for="password" class="form-label">
<i class="fas fa-lock me-2"></i>Password
</label>
<div class="input-group">
<span class="input-group-text bg-light">
<i class="fas fa-lock text-primary"></i>
</span>
<input type="password" class="form-control" id="password" name="password"
placeholder="Enter your password" required>
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword()">
<i class="fas fa-eye" id="togglePasswordIcon"></i>
</button>
</div>
</div>
<div class="mb-4 d-flex justify-content-between align-items-center">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="remember" name="remember">
<label class="form-check-label" for="remember">Remember me</label>
</div>
<a href="forgot_password.php" class="text-decoration-none">Forgot Password?</a>
</div>
<button type="submit" class="btn btn-primary w-100 py-2 mb-3">
<i class="fas fa-sign-in-alt me-2"></i>Login
</button>
</form>
<div class="text-center">
<p class="mb-2">Don't have an account?</p>
<div class="d-grid gap-2">
<a href="register.php?role=buyer" class="btn btn-outline-primary">
<i class="fas fa-user me-2"></i>Sign up as Buyer
</a>
<a href="register.php?role=agent" class="btn btn-outline-success">
<i class="fas fa-user-tie me-2"></i>Sign up as Agent
</a>
<a href="register.php?role=seller" class="btn btn-outline-info">
<i class="fas fa-building me-2"></i>Sign up as Seller
</a>
</div>
</div>
<hr class="my-4">
<div class="text-center">
<a href="index.php" class="text-decoration-none">
<i class="fas fa-arrow-left me-1"></i>Back to Home
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script>
function togglePassword() {
const password = document.getElementById('password');
const icon = document.getElementById('togglePasswordIcon');
if (password.type === 'password') {
password.type = 'text';
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
} else {
password.type = 'password';
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
}
function validateLogin() {
const email = document.getElementById('email').value.trim();
const password = document.getElementById('password').value;
if (email === '') {
alert('Please enter your email');
return false;
}
if (password === '') {
alert('Please enter your password');
return false;
}
return true;
}
</script>
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.card {
backdrop-filter: blur(10px);
}
</style>
</body>
</html>
Registration Page
File: register.php
<?php
require_once 'includes/config.php';
// Redirect if already logged in
if ($auth->isLoggedIn()) {
redirect('/index.php');
}
$role = $_GET['role'] ?? 'buyer';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Sanitize inputs
$data = [
'email' => sanitize($_POST['email'] ?? ''),
'password' => $_POST['password'] ?? '',
'confirm_password' => $_POST['confirm_password'] ?? '',
'first_name' => sanitize($_POST['first_name'] ?? ''),
'last_name' => sanitize($_POST['last_name'] ?? ''),
'phone' => sanitize($_POST['phone'] ?? ''),
'role' => $_POST['role'] ?? 'buyer'
];
// Add agent-specific fields
if ($data['role'] === 'agent') {
$data['license_number'] = sanitize($_POST['license_number'] ?? '');
$data['agency_name'] = sanitize($_POST['agency_name'] ?? '');
$data['years_experience'] = intval($_POST['years_experience'] ?? 0);
$data['specializations'] = sanitize($_POST['specializations'] ?? '');
$data['bio'] = sanitize($_POST['bio'] ?? '');
}
// Validate inputs
$errors = [];
if (empty($data['email'])) {
$errors[] = 'Email is required';
} elseif (!validateEmail($data['email'])) {
$errors[] = 'Please enter a valid email address';
}
if (empty($data['password'])) {
$errors[] = 'Password is required';
} elseif (strlen($data['password']) < 8) {
$errors[] = 'Password must be at least 8 characters';
} elseif (!preg_match('/[A-Z]/', $data['password'])) {
$errors[] = 'Password must contain at least one uppercase letter';
} elseif (!preg_match('/[a-z]/', $data['password'])) {
$errors[] = 'Password must contain at least one lowercase letter';
} elseif (!preg_match('/[0-9]/', $data['password'])) {
$errors[] = 'Password must contain at least one number';
}
if ($data['password'] !== $data['confirm_password']) {
$errors[] = 'Passwords do not match';
}
if (empty($data['first_name'])) {
$errors[] = 'First name is required';
}
if (empty($data['last_name'])) {
$errors[] = 'Last name is required';
}
// If no errors, attempt registration
if (empty($errors)) {
$result = $auth->register($data);
if ($result['success']) {
$_SESSION['success'] = 'Registration successful! Please check your email to verify your account.';
redirect('/login.php');
} else {
$error = $result['error'];
}
} else {
$error = implode('<br>', $errors);
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - <?php echo SITE_NAME; ?></title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="bg-light">
<div class="container">
<div class="row justify-content-center align-items-center min-vh-100 py-4">
<div class="col-md-8 col-lg-6">
<div class="card shadow-lg border-0 rounded-4">
<div class="card-header bg-primary text-white text-center py-4 rounded-top-4">
<h3 class="mb-0">
<i class="fas fa-user-plus me-2"></i>
Create Account
</h3>
<p class="mb-0 text-white-50">Join as a <?php echo ucfirst($role); ?></p>
</div>
<div class="card-body p-5">
<?php if ($error): ?>
<div class="alert alert-danger alert-dismissible fade show">
<i class="fas fa-exclamation-circle me-2"></i>
<?php echo $error; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<!-- Role Tabs -->
<ul class="nav nav-pills nav-justified mb-4">
<li class="nav-item">
<a class="nav-link <?php echo $role === 'buyer' ? 'active' : ''; ?>"
href="?role=buyer">
<i class="fas fa-user me-2"></i>Buyer
</a>
</li>
<li class="nav-item">
<a class="nav-link <?php echo $role === 'agent' ? 'active' : ''; ?>"
href="?role=agent">
<i class="fas fa-user-tie me-2"></i>Agent
</a>
</li>
<li class="nav-item">
<a class="nav-link <?php echo $role === 'seller' ? 'active' : ''; ?>"
href="?role=seller">
<i class="fas fa-building me-2"></i>Seller
</a>
</li>
</ul>
<form method="POST" action="" onsubmit="return validateRegistration()">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<input type="hidden" name="role" value="<?php echo $role; ?>">
<div class="row">
<div class="col-md-6 mb-3">
<label for="first_name" class="form-label">First Name *</label>
<input type="text" class="form-control" id="first_name" name="first_name"
value="<?php echo htmlspecialchars($_POST['first_name'] ?? ''); ?>" required>
</div>
<div class="col-md-6 mb-3">
<label for="last_name" class="form-label">Last Name *</label>
<input type="text" class="form-control" id="last_name" name="last_name"
value="<?php echo htmlspecialchars($_POST['last_name'] ?? ''); ?>" required>
</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email Address *</label>
<input type="email" class="form-control" id="email" name="email"
value="<?php echo htmlspecialchars($_POST['email'] ?? ''); ?>" required>
</div>
<div class="mb-3">
<label for="phone" class="form-label">Phone Number</label>
<input type="tel" class="form-control" id="phone" name="phone"
value="<?php echo htmlspecialchars($_POST['phone'] ?? ''); ?>">
</div>
<!-- Agent-specific fields -->
<?php if ($role === 'agent'): ?>
<div class="mb-3">
<label for="license_number" class="form-label">License Number</label>
<input type="text" class="form-control" id="license_number" name="license_number"
value="<?php echo htmlspecialchars($_POST['license_number'] ?? ''); ?>">
</div>
<div class="mb-3">
<label for="agency_name" class="form-label">Agency Name</label>
<input type="text" class="form-control" id="agency_name" name="agency_name"
value="<?php echo htmlspecialchars($_POST['agency_name'] ?? ''); ?>">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="years_experience" class="form-label">Years Experience</label>
<input type="number" class="form-control" id="years_experience" name="years_experience"
value="<?php echo htmlspecialchars($_POST['years_experience'] ?? '0'); ?>">
</div>
<div class="col-md-6 mb-3">
<label for="specializations" class="form-label">Specializations</label>
<input type="text" class="form-control" id="specializations" name="specializations"
value="<?php echo htmlspecialchars($_POST['specializations'] ?? ''); ?>"
placeholder="e.g., Residential, Commercial">
</div>
</div>
<div class="mb-3">
<label for="bio" class="form-label">Bio</label>
<textarea class="form-control" id="bio" name="bio" rows="3"><?php echo htmlspecialchars($_POST['bio'] ?? ''); ?></textarea>
</div>
<?php endif; ?>
<div class="row">
<div class="col-md-6 mb-3">
<label for="password" class="form-label">Password *</label>
<div class="input-group">
<input type="password" class="form-control" id="password" name="password" required>
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('password')">
<i class="fas fa-eye" id="togglePasswordIcon1"></i>
</button>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="confirm_password" class="form-label">Confirm Password *</label>
<div class="input-group">
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('confirm_password')">
<i class="fas fa-eye" id="togglePasswordIcon2"></i>
</button>
</div>
</div>
</div>
<div class="mb-3">
<div class="password-requirements">
<small class="text-muted">
Password must contain:
<span id="req-length" class="req-badge">✓ 8+ characters</span>
<span id="req-uppercase" class="req-badge">✓ Uppercase</span>
<span id="req-lowercase" class="req-badge">✓ Lowercase</span>
<span id="req-number" class="req-badge">✓ Number</span>
</small>
</div>
</div>
<div class="mb-4 form-check">
<input type="checkbox" class="form-check-input" id="terms" name="terms" required>
<label class="form-check-label" for="terms">
I agree to the <a href="#" target="_blank">Terms of Service</a> and
<a href="#" target="_blank">Privacy Policy</a>
</label>
</div>
<button type="submit" class="btn btn-primary w-100 py-2 mb-3">
<i class="fas fa-user-plus me-2"></i>Create Account
</button>
</form>
<div class="text-center">
<p class="mb-0">
Already have an account?
<a href="login.php" class="text-decoration-none">Sign In</a>
</p>
</div>
<hr class="my-4">
<div class="text-center">
<a href="index.php" class="text-decoration-none">
<i class="fas fa-arrow-left me-1"></i>Back to Home
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script>
function togglePassword(fieldId) {
const password = document.getElementById(fieldId);
const icon = document.getElementById('togglePasswordIcon' + (fieldId === 'password' ? '1' : '2'));
if (password.type === 'password') {
password.type = 'text';
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
} else {
password.type = 'password';
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
}
function validateRegistration() {
const password = document.getElementById('password').value;
const confirm = document.getElementById('confirm_password').value;
const terms = document.getElementById('terms').checked;
// Check password requirements
const hasLength = password.length >= 8;
const hasUppercase = /[A-Z]/.test(password);
const hasLowercase = /[a-z]/.test(password);
const hasNumber = /[0-9]/.test(password);
if (!hasLength || !hasUppercase || !hasLowercase || !hasNumber) {
alert('Please meet all password requirements');
return false;
}
if (password !== confirm) {
alert('Passwords do not match');
return false;
}
if (!terms) {
alert('You must agree to the Terms of Service');
return false;
}
return true;
}
// Real-time password validation
document.getElementById('password').addEventListener('input', function() {
const password = this.value;
document.getElementById('req-length').style.color = password.length >= 8 ? 'green' : 'red';
document.getElementById('req-uppercase').style.color = /[A-Z]/.test(password) ? 'green' : 'red';
document.getElementById('req-lowercase').style.color = /[a-z]/.test(password) ? 'green' : 'red';
document.getElementById('req-number').style.color = /[0-9]/.test(password) ? 'green' : 'red';
});
</script>
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.password-requirements {
background: #f8f9fa;
padding: 10px;
border-radius: 5px;
}
.req-badge {
display: inline-block;
margin-right: 10px;
font-size: 0.85rem;
}
.nav-pills .nav-link {
color: #6c757d;
}
.nav-pills .nav-link.active {
background-color: #0d6efd;
}
</style>
</body>
</html>
Environment Configuration
File: .env
# Database Configuration DB_HOST=localhost DB_NAME=real_estate DB_USER=root DB_PASS= # Application Configuration APP_NAME=Real Estate Platform APP_URL=http://localhost/real-estate APP_VERSION=1.0.0 DEBUG_MODE=true # Security SESSION_TIMEOUT=3600 BCRYPT_ROUNDS=12 # Upload Configuration MAX_FILE_SIZE=10485760 # 10MB in bytes # Pagination PROPERTIES_PER_PAGE=12 AGENTS_PER_PAGE=12 # Date/Time TIMEZONE=America/New_York DATE_FORMAT=M d, Y TIME_FORMAT=h:i A # Currency CURRENCY=USD CURRENCY_SYMBOL=$ # Image Configuration IMAGE_QUALITY=85 THUMB_WIDTH=400 THUMB_HEIGHT=300 MEDIUM_WIDTH=800 MEDIUM_HEIGHT=600 LARGE_WIDTH=1200 LARGE_HEIGHT=900 # Map Configuration MAP_API_KEY=your_google_maps_api_key MAP_CENTER_LAT=40.7128 MAP_CENTER_LNG=-74.0060 MAP_ZOOM=10 # Email Configuration (for production) SMTP_HOST=smtp.gmail.com SMTP_PORT=587 [email protected] SMTP_PASS=your_password SMTP_ENCRYPTION=tls
File: .gitignore
# Environment variables .env # Dependencies /vendor/ node_modules/ # IDE files .vscode/ .idea/ *.sublime-* # OS files .DS_Store Thumbs.db # Logs /logs/ *.log # Uploads /uploads/ !/uploads/.gitkeep !/uploads/properties/.gitkeep !/uploads/agents/.gitkeep !/uploads/users/.gitkeep # Cache /cache/ !/cache/.gitkeep # Composer composer.lock # Temp files *.tmp *.temp
File: composer.json
{
"name": "real-estate/platform",
"description": "Real Estate Listing Website",
"type": "project",
"require": {
"php": ">=7.4",
"ext-pdo": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-curl": "*",
"ext-gd": "*",
"phpmailer/phpmailer": "^6.8",
"intervention/image": "^2.7"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
},
"autoload": {
"psr-4": {
"RealEstate\\": "src/"
}
},
"scripts": {
"test": "phpunit tests",
"post-install-cmd": [
"chmod -R 755 uploads/",
"chmod -R 755 logs/",
"chmod -R 755 cache/"
]
}
}
How to Use the Project (Step-by-Step Guide)
Prerequisites
- Web Server: XAMPP, WAMP, MAMP, or any PHP-enabled server (PHP 7.4+)
- Database: MySQL 8.0+ or MariaDB
- Composer: For dependency management
- Node.js (optional): For frontend asset compilation
- Browser: Modern web browser (Chrome, Firefox, Edge, etc.)
Installation Steps
Step 1: Set Up Local Server
- Download and install XAMPP from https://www.apachefriends.org/
- Launch XAMPP Control Panel
- Start Apache and MySQL services
Step 2: Install Composer Dependencies
- Download and install Composer from https://getcomposer.org/
- Navigate to your project directory in terminal
- Run:
composer install
Step 3: Create Project Folder
- Navigate to
C:\xampp\htdocs\(Windows) or/Applications/XAMPP/htdocs/(Mac) - Create a new folder named
real-estate - Create all the folders and files as shown in the Project File Structure
Step 4: Set Up Database
- Open browser and go to
http://localhost/phpmyadmin - Click on "New" to create a new database
- Name the database
real_estateand selectutf8_general_ci - Click on "Import" tab
- Click "Choose File" and select the
database.sqlfile from thesqlfolder - Click "Go" to import the database structure and sample data
Step 5: Configure Environment
- Rename
.env.exampleto.envin the project root - Update database credentials:
DB_HOST=localhost DB_NAME=real_estate DB_USER=root DB_PASS=
- Update application URL:
APP_URL=http://localhost/real-estate
- Add Google Maps API key (optional but recommended):
MAP_API_KEY=your_google_maps_api_key
Step 6: Set Folder Permissions
Create the following folders and ensure they are writable:
uploads/properties/uploads/agents/uploads/users/logs/cache/
On Windows, right-click folders → Properties → Security → give Write permission to Users
On Mac/Linux, run: chmod -R 755 uploads/ logs/ cache/
Step 7: Create Admin User
- Go to
http://localhost/real-estate/register.php - Register as a buyer with email:
[email protected] - Open phpMyAdmin, go to the
userstable - Find the user and change:
roletoadminstatustoactiveemail_verifiedto1
Step 8: Test the Installation
- Open browser and go to
http://localhost/real-estate/ - You should see the landing page with featured properties
- Test different user types: Admin Login:
- Email:
[email protected] - Password: (the password you set during registration) Agent Registration:
- Click "Sign up as Agent" and register
- Wait for admin approval or set status to active in database Buyer/Seller Registration:
- Register as a regular user
- Browse and search for properties
System Walkthrough
For Buyers:
- Browse Properties - Use search filters to find properties
- View Details - Check property images, features, and location
- Save Favorites - Save interesting properties to wishlist
- Contact Agent - Send inquiries about properties
- Schedule Viewings - Book appointments to visit properties
- Mortgage Calculator - Calculate monthly payments
For Sellers:
- List Properties - Add properties for sale
- Upload Media - Add photos and virtual tours
- Manage Listings - Track property views and inquiries
- Find Agents - Connect with real estate agents
- Valuation Tool - Get estimated property value
For Agents:
- Dashboard - View listing statistics and inquiries
- Manage Properties - Add and manage client properties
- Respond to Inquiries - Answer buyer questions
- Schedule Viewings - Manage appointment calendar
- Subscription - Upgrade plan for more features
For Admins:
- Dashboard - View platform statistics
- User Management - Approve agents and manage users
- Property Moderation - Approve/reject property listings
- Category Management - Manage property types and amenities
- Reports - Generate platform reports
- Settings - Configure system settings
Troubleshooting
Common Issues and Solutions
- Database Connection Error
- Check if MySQL is running
- Verify database credentials in
.env - Ensure database
real_estateexists
- 404 Page Not Found
- Check file paths and folder structure
- Verify
APP_URLin.env - Ensure
.htaccessis properly configured (if using Apache)
- Image Upload Issues
- Check folder permissions (uploads/ must be writable)
- Verify
MAX_FILE_SIZEin.env - Check PHP upload limits in
php.ini
- Map Not Displaying
- Add valid Google Maps API key in
.env - Ensure latitude/longitude are set for properties
- Email Not Sending
- Configure SMTP settings in
.env - Check spam folder
- For local development, use tools like MailHog or Mailtrap
Security Best Practices
- Change default admin password immediately after installation
- Use HTTPS in production with SSL certificate
- Regular backups of database and uploads
- Input validation on both client and server side
- SQL injection prevention using prepared statements
- XSS prevention using
htmlspecialchars() - CSRF tokens for all forms
- Password hashing using bcrypt
- Rate limiting for login attempts
- Session security with proper timeout
Performance Optimizations
- Database indexing on frequently queried columns
- Query caching for repeated requests
- Image optimization using Intervention Image
- Lazy loading for images and content
- Pagination for large datasets
- CDN for Bootstrap, Font Awesome, and jQuery
- Browser caching for static assets
- Gzip compression for faster loading
Conclusion
The Real Estate Listing Website is a comprehensive, feature-rich platform for buying, selling, and renting properties. With its intuitive interface, powerful search capabilities, and role-based access control, it provides everything needed for a modern real estate marketplace.
This application demonstrates:
- Advanced search with multiple filters
- Map integration for property locations
- Image galleries and virtual tours
- User authentication with role-based access
- Property management for agents and sellers
- Inquiry system for buyer-agent communication
- Favorites and saved searches
- Mortgage calculator for buyers
- Subscription plans for agents
- Responsive design for all devices
- Admin dashboard for platform management
- Analytics and reporting
- SEO-friendly URLs with slugs
The system is built to be scalable, secure, and customizable, allowing easy addition of new features like property comparisons, neighborhood guides, school ratings, and more. Whether you're building a local real estate website or a national platform, this system provides a solid foundation that can be extended to meet specific business requirements.