Building a Dating Website in PHP: the ultimate tutorial in 10 parts

Part 1: Laying the Foundation

Welcome, brave warrior of web development, to what might be the most important coding journey of your life! Today we begin building a dating website - not just for the glory, but for the possibility that maybe, just maybe, you'll actually get to use your own creation. wink

Think of this as building a digital bar, but with less spilled beer and more SQL injections to worry about. Let's get started!

798f642489e4658a47e8e39412e5d16a.png

What We're Building Today

In Part 1, we're setting up the foundation - user registration and authentication. Because let's face it, without users, your dating site is just you staring at an empty database. And that's just sad.

File Structure Setup

First, let's create our project structure. Create these files and folders:

/your-dating-site/
├── config/
│   └── database.php
├── includes/
│   ├── header.php
│   ├── footer.php
│   └── functions.php
├── assets/
│   ├── css/
│   ├── js/
│   └── images/
├── register.php
├── login.php
├── dashboard.php
├── logout.php
└── index.php

1. Database Configuration

Let's start with the heart of our operation - the database. Create config/database.php:

// config/database.php

class Database {
    private $host = "localhost";
    private $db_name = "dating_site";
    private $username = "root";
    private $password = "";
    public $conn;

    // Get that database connection flowing like cheap beer at a frat party
    public function getConnection() {
        $this->conn = null;

        try {
            $this->conn = new PDO(
                "mysql:host=" . $this->host . ";dbname=" . $this->db_name, 
                $this->username, 
                $this->password
            );
            $this->conn->exec("set names utf8");

            // If you see this message, the database likes you!
            error_log("Database connection established successfully");

        } catch(PDOException $exception) {
            // If you see this, the database rejected you. It's not you, it's... actually yeah, it's you.
            error_log("Connection error: " . $exception->getMessage());
        }

        return $this->conn;
    }
}

2. Database Schema

Before we go further, let's create our users table. Run this SQL in your MySQL database:

CREATE DATABASE dating_site;
USE dating_site;

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) 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,
    gender ENUM('male', 'female', 'other') NOT NULL,
    date_of_birth DATE NOT NULL,
    profile_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_login TIMESTAMP NULL,
    is_active TINYINT(1) DEFAULT 1,

    -- Because even in dating, we need to know who's the boss
    INDEX idx_username (username),
    INDEX idx_email (email),
    INDEX idx_gender (gender)
);

-- Let's add a default admin user because someone's gotta be in charge
-- Password is 'admin123' (hashed)
INSERT INTO users (username, email, password, first_name, last_name, gender, date_of_birth) 
VALUES ('admin', 'admin@datingsite.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Big', 'Boss', 'male', '1980-01-01');

3. Header Template

Now let's create our header in includes/header.php:

// includes/header.php
session_start();

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MateFinder - Where Connections Happen (Hopefully)</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
        }
        .navbar {
            background: rgba(255, 255, 255, 0.95);
            padding: 1rem 0;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .container {
            width: 90%;
            max-width: 1200px;
            margin: 0 auto;
        }
        .nav-content {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .logo {
            font-size: 1.5rem;
            font-weight: bold;
            color: #764ba2;
            text-decoration: none;
        }
        .nav-links a {
            margin-left: 2rem;
            text-decoration: none;
            color: #333;
            font-weight: 500;
        }
        .main-content {
            background: white;
            margin: 2rem auto;
            padding: 2rem;
            border-radius: 10px;
            box-shadow: 0 5px 15px rgba(0,0,0,0.1);
        }
        .btn {
            background: #764ba2;
            color: white;
            padding: 0.5rem 1rem;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            text-decoration: none;
            display: inline-block;
        }
        .btn:hover {
            background: #667eea;
        }
    </style>
</head>
<body>
    <nav class="navbar">
        <div class="container">
            <div class="nav-content">
                <a href="index.php" class="logo">🔥 MateFinder</a>
                <div class="nav-links">
                     if(isset($_SESSION['user_id'])): 
                        <!-- When user is logged in - show them the goods -->
                        <a href="dashboard.php">Dashboard</a>
                        <a href="logout.php" class="btn">Logout</a>
                     else: 
                        <!-- When user is not logged in - tease them a little -->
                        <a href="login.php">Login</a>
                        <a href="register.php" class="btn">Find Your Match</a>
                     endif; 
                </div>
            </div>
        </div>
    </nav>
    <div class="container">
        <div class="main-content">

4. Footer Template

Create includes/footer.php:

// includes/footer.php

        </div> <!-- Close main-content -->

        <footer style="text-align: center; color: white; padding: 2rem 0;">
            <p>&copy;  echo date('Y');  MateFinder. 
               Because your mom keeps asking when you'll settle down.</p>
            <p style="font-size: 0.8rem; opacity: 0.7;">
                Disclaimer: We're not responsible for bad dates, awkward conversations, 
                or your questionable taste in partners.
            </p>
        </footer>
    </div> <!-- Close container -->
</body>
</html>

5. Helper Functions

Now for our Swiss Army knife - includes/functions.php:

// includes/functions.php

// Sanitize user input because people will try to break your stuff
// It's like expecting your drunk friend not to spill his drink - you gotta prepare
function sanitize_input($data) {
    $data = trim($data);
    $data = stripslashes($data);
    $data = htmlspecialchars($data);
    return $data;
}

// Validate email - because "notAnEmail" probably won't work
function validate_email($email) {
    return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}

// Check if username is available
// This is like checking if your favorite bar stool is taken
function is_username_available($username, $conn) {
    $query = "SELECT id FROM users WHERE username = :username";
    $stmt = $conn->prepare($query);
    $stmt->bindParam(':username', $username);
    $stmt->execute();

    return $stmt->rowCount() === 0;
}

// User login function
// Returns user data if successful, false if not
function login_user($username, $password, $conn) {
    $query = "SELECT id, username, password, first_name FROM users 
              WHERE username = :username AND is_active = 1";
    $stmt = $conn->prepare($query);
    $stmt->bindParam(':username', $username);
    $stmt->execute();

    if ($stmt->rowCount() === 1) {
        $user = $stmt->fetch(PDO::FETCH_ASSOC);

        // Verify password - don't worry, it's not stored in plain text
        // That would be like leaving your house key under the doormat
        if (password_verify($password, $user['password'])) {
            // Update last login time
            $update_query = "UPDATE users SET last_login = NOW() WHERE id = :id";
            $update_stmt = $conn->prepare($update_query);
            $update_stmt->bindParam(':id', $user['id']);
            $update_stmt->execute();

            return $user;
        }
    }

    return false;
}

// Check if user is logged in
// This is like the bouncer at the club door
function is_logged_in() {
    return isset($_SESSION['user_id']);
}

// Redirect function with optional message
function redirect($url, $message = null) {
    if ($message) {
        $_SESSION['flash_message'] = $message;
    }
    header("Location: $url");
    exit();
}

// Get flash message and clear it
// Like reading a note then throwing it away
function get_flash_message() {
    if (isset($_SESSION['flash_message'])) {
        $message = $_SESSION['flash_message'];
        unset($_SESSION['flash_message']);
        return $message;
    }
    return null;
}

6. Registration Page

Now for the main event - register.php:

// register.php
require_once 'config/database.php';
require_once 'includes/functions.php';

$database = new Database();
$conn = $database->getConnection();

$errors = [];
$success = false;

// If form is submitted, let's process this love connection
if ($_POST) {
    // Sanitize all inputs - trust no one!
    $username = sanitize_input($_POST['username'] ?? '');
    $email = sanitize_input($_POST['email'] ?? '');
    $password = $_POST['password'] ?? '';
    $confirm_password = $_POST['confirm_password'] ?? '';
    $first_name = sanitize_input($_POST['first_name'] ?? '');
    $last_name = sanitize_input($_POST['last_name'] ?? '');
    $gender = sanitize_input($_POST['gender'] ?? '');
    $date_of_birth = sanitize_input($_POST['date_of_birth'] ?? '');

    // Validation - because people will try to register as "Mickey Mouse"
    if (empty($username)) {
        $errors[] = "Username is required. Unless you want to be called 'User1234'...";
    } elseif (!is_username_available($username, $conn)) {
        $errors[] = "Username already taken. Time to get more creative!";
    }

    if (empty($email) || !validate_email($email)) {
        $errors[] = "Valid email is required. We promise not to send too many spam emails!";
    }

    if (empty($password)) {
        $errors[] = "Password is required. 'password123' is not recommended.";
    } elseif (strlen($password) < 6) {
        $errors[] = "Password must be at least 6 characters. Make it stronger than your ex's grip on your hoodie.";
    } elseif ($password !== $confirm_password) {
        $errors[] = "Passwords don't match. You had one job!";
    }

    // Calculate age from date of birth
    $birth_date = new DateTime($date_of_birth);
    $today = new DateTime();
    $age = $today->diff($birth_date)->y;

    if ($age < 18) {
        $errors[] = "You must be at least 18 years old. Sorry, kid - come back when you can legally buy your date a drink.";
    }

    // If no errors, let's create this user!
    if (empty($errors)) {
        $hashed_password = password_hash($password, PASSWORD_DEFAULT);

        $query = "INSERT INTO users 
                  (username, email, password, first_name, last_name, gender, date_of_birth) 
                  VALUES 
                  (:username, :email, :password, :first_name, :last_name, :gender, :date_of_birth)";

        $stmt = $conn->prepare($query);

        $stmt->bindParam(':username', $username);
        $stmt->bindParam(':email', $email);
        $stmt->bindParam(':password', $hashed_password);
        $stmt->bindParam(':first_name', $first_name);
        $stmt->bindParam(':last_name', $last_name);
        $stmt->bindParam(':gender', $gender);
        $stmt->bindParam(':date_of_birth', $date_of_birth);

        if ($stmt->execute()) {
            $success = true;
            // Redirect to login after 3 seconds to show success message
            header("refresh:3;url=login.php");
        } else {
            $errors[] = "Something went wrong. Probably not your fault. Probably.";
        }
    }
}

include_once 'includes/header.php';

<h1>Join MateFinder - Your Future Self Will Thank You</h1>
<p>Fill out this form. It's easier than asking for someone's number at the gym.</p>

 if ($success): 
    <div style="background: #d4edda; color: #155724; padding: 1rem; border-radius: 5px; margin: 1rem 0;">
        <strong>Success!</strong> Your account has been created. 
        Redirecting you to login... unless you closed your eyes and missed this message.
    </div>
 endif; 

 if (!empty($errors)): 
    <div style="background: #f8d7da; color: #721c24; padding: 1rem; border-radius: 5px; margin: 1rem 0;">
        <strong>Oops! Some issues:</strong>
        <ul>
             foreach ($errors as $error): 
                <li> echo $error; </li>
             endforeach; 
        </ul>
    </div>
 endif; 

<form method="POST" action="" style="max-width: 500px;">
    <div style="margin-bottom: 1rem;">
        <label for="username">Username:</label>
        <input type="text" id="username" name="username" required 
               style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;"
               value=" echo $_POST['username'] ?? ''; ">
    </div>

    <div style="margin-bottom: 1rem;">
        <label for="email">Email:</label>
        <input type="email" id="email" name="email" required 
               style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;"
               value=" echo $_POST['email'] ?? ''; ">
    </div>

    <div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
        <div style="flex: 1;">
            <label for="first_name">First Name:</label>
            <input type="text" id="first_name" name="first_name" required 
                   style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;"
                   value=" echo $_POST['first_name'] ?? ''; ">
        </div>
        <div style="flex: 1;">
            <label for="last_name">Last Name:</label>
            <input type="text" id="last_name" name="last_name" required 
                   style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;"
                   value=" echo $_POST['last_name'] ?? ''; ">
        </div>
    </div>

    <div style="margin-bottom: 1rem;">
        <label for="gender">Gender:</label>
        <select id="gender" name="gender" required 
                style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;">
            <option value="">Select...</option>
            <option value="male"  echo ($_POST['gender'] ?? '') === 'male' ? 'selected' : ''; >Male</option>
            <option value="female"  echo ($_POST['gender'] ?? '') === 'female' ? 'selected' : ''; >Female</option>
            <option value="other"  echo ($_POST['gender'] ?? '') === 'other' ? 'selected' : ''; >Other</option>
        </select>
    </div>

    <div style="margin-bottom: 1rem;">
        <label for="date_of_birth">Date of Birth:</label>
        <input type="date" id="date_of_birth" name="date_of_birth" required 
               style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;"
               value=" echo $_POST['date_of_birth'] ?? ''; ">
    </div>

    <div style="margin-bottom: 1rem;">
        <label for="password">Password:</label>
        <input type="password" id="password" name="password" required 
               style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;">
    </div>

    <div style="margin-bottom: 1rem;">
        <label for="confirm_password">Confirm Password:</label>
        <input type="password" id="confirm_password" name="confirm_password" required 
               style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;">
    </div>

    <button type="submit" class="btn" style="width: 100%; padding: 0.75rem;">
        Create Account - Adventure Awaits!
    </button>
</form>

<p style="margin-top: 2rem; text-align: center;">
    Already have an account? <a href="login.php">Login here</a>. 
    Your matches are waiting!
</p>

 include_once 'includes/footer.php'; 

What We've Accomplished So Far

Congratulations, you magnificent coding machine! In Part 1, we've built:

  1. Database foundation with proper user table structure
  2. Secure authentication system with password hashing
  3. User registration with validation that would make a bouncer proud
  4. Reusable templates so we don't repeat ourselves (DRY principle, bro!)
  5. Helper functions that do the heavy lifting

What's Coming in Part 2

In the next installment, we'll tackle:

Remember: This is just the foundation. We're building the bar before we stock it with drinks and attractive people.

Current Status: Your dating site can now register users. It's like having a guest list for a party where nobody has shown up yet. But hey, you've got the list!

Stay tuned for Part 2, where we'll actually let people in the door!


Pro Tip: Test your registration form thoroughly. Try breaking it. Users certainly will. It's like stress-testing your couch before your 300-pound uncle comes over for the game.

Building a Dating Website in PHP - Part 2: Letting People In The Door

Welcome back, you glorious coding warrior! In Part 1, we built the registration system - the digital equivalent of printing "Guest List" on a fancy piece of paper. Now, we're actually going to let people in the club. Time to build the login system and make this thing actually usable!

What We're Building in Part 2

1. Login System

Create login.php - this is where users prove they're not imposters:


// login.php
require_once 'config/database.php';
require_once 'includes/functions.php';

$database = new Database();
$conn = $database->getConnection();

$errors = [];

// If someone's trying to break in (politely, with credentials)
if ($_POST) {
    $username = sanitize_input($_POST['username'] ?? '');
    $password = $_POST['password'] ?? '';

    // Basic validation - because "admin/admin" is too obvious
    if (empty($username) || empty($password)) {
        $errors[] = "Both username and password are required. This isn't a choose-your-own-adventure book.";
    }

    if (empty($errors)) {
        // Try to log them in - like a bouncer checking IDs
        $user = login_user($username, $password, $conn);

        if ($user) {
            // Login successful! Set session and redirect
            $_SESSION['user_id'] = $user['id'];
            $_SESSION['username'] = $user['username'];
            $_SESSION['first_name'] = $user['first_name'];

            // Welcome message that makes them feel special
            $_SESSION['flash_message'] = "Welcome back, " . $user['first_name'] . "! Your matches are waiting.";

            redirect('dashboard.php');
        } else {
            $errors[] = "Invalid username or password. Did you forget already? It's only been 5 minutes...";
        }
    }
}

// Check if user is already logged in
// If they are, redirect them to dashboard
// This is like telling someone who's already inside the club to stop waiting in line
if (is_logged_in()) {
    redirect('dashboard.php');
}

include_once 'includes/header.php';

<h1>Welcome Back! We Missed Your Face</h1>
<p>Login below. Your potential soulmate is just one correct password away.</p>

// Show flash message if it exists
$flash_message = get_flash_message();
if ($flash_message): 
    <div style="background: #d1ecf1; color: #0c5460; padding: 1rem; border-radius: 5px; margin: 1rem 0;">
         echo $flash_message; 
    </div>
 endif; 

 if (!empty($errors)): 
    <div style="background: #f8d7da; color: #721c24; padding: 1rem; border-radius: 5px; margin: 1rem 0;">
        <strong>Login Failed:</strong>
        <ul>
             foreach ($errors as $error): 
                <li> echo $error; </li>
             endforeach; 
        </ul>
    </div>
 endif; 

<div style="max-width: 400px; margin: 0 auto;">
    <form method="POST" action="">
        <div style="margin-bottom: 1rem;">
            <label for="username">Username:</label>
            <input type="text" id="username" name="username" required 
                   style="width: 100%; padding: 0.75rem; margin-top: 0.5rem; border: 1px solid #ddd; border-radius: 5px;"
                   value=" echo $_POST['username'] ?? ''; "
                   placeholder="Enter your username">
        </div>

        <div style="margin-bottom: 1.5rem;">
            <label for="password">Password:</label>
            <input type="password" id="password" name="password" required 
                   style="width: 100%; padding: 0.75rem; margin-top: 0.5rem; border: 1px solid #ddd; border-radius: 5px;"
                   placeholder="Enter your password">
        </div>

        <button type="submit" class="btn" style="width: 100%; padding: 0.75rem; font-size: 1.1rem;">
            Let Me In! 🚀
        </button>
    </form>

    <div style="text-align: center; margin-top: 2rem; padding-top: 2rem; border-top: 1px solid #eee;">
        <p>New around here? Don't be shy!</p>
        <a href="register.php" class="btn" style="background: #28a745;">
            Create Account - It's Free! 🎉
        </a>
    </div>
</div>

 include_once 'includes/footer.php'; 

2. Dashboard - Where The Magic Happens

Create dashboard.php - this is the main attraction:


// dashboard.php
require_once 'config/database.php';
require_once 'includes/functions.php';

// Check if user is logged in
// If not, redirect to login faster than someone realizing they've been ghosted
if (!is_logged_in()) {
    redirect('login.php', "You need to login first. Your matches can't wait to meet you!");
}

$database = new Database();
$conn = $database->getConnection();

// Get user data - because personalization is key
$user_id = $_SESSION['user_id'];
$query = "SELECT first_name, last_name, email, gender, date_of_birth, profile_created, last_login 
          FROM users WHERE id = :id";
$stmt = $conn->prepare($query);
$stmt->bindParam(':id', $user_id);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);

// Calculate age from date of birth
$birth_date = new DateTime($user['date_of_birth']);
$today = new DateTime();
$age = $today->diff($birth_date)->y;

include_once 'includes/header.php';

// Show flash message if it exists
$flash_message = get_flash_message();
if ($flash_message): 
    <div style="background: #d4edda; color: #155724; padding: 1rem; border-radius: 5px; margin: 1rem 0;">
         echo $flash_message; 
    </div>
 endif; 

<h1>Welcome to Your Dashboard,  echo $user['first_name']; ! 👑</h1>
<p>This is your command center. Your dating mission control. Your... well, you get the idea.</p>

<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 2rem; margin-top: 2rem;">

    <!-- User Profile Card -->
    <div style="background: #f8f9fa; padding: 1.5rem; border-radius: 10px; border-left: 4px solid #764ba2;">
        <h3 style="margin-top: 0; color: #764ba2;">Your Profile</h3>

        <div style="margin-bottom: 1rem;">
            <strong>Name:</strong>  echo $user['first_name'] . ' ' . $user['last_name']; 
        </div>

        <div style="margin-bottom: 1rem;">
            <strong>Age:</strong>  echo $age;  years young
        </div>

        <div style="margin-bottom: 1rem;">
            <strong>Gender:</strong> 
            <span style="text-transform: capitalize;"> echo $user['gender']; </span>
        </div>

        <div style="margin-bottom: 1rem;">
            <strong>Member since:</strong> 
             echo date('F j, Y', strtotime($user['profile_created'])); 
        </div>

        <div style="margin-bottom: 1rem;">
            <strong>Last login:</strong> 
             echo $user['last_login'] ? date('F j, Y g:i A', strtotime($user['last_login'])) : 'First time!'; 
        </div>

        <a href="#" class="btn" style="width: 100%; text-align: center; margin-top: 1rem;">
            Edit Profile (Coming Soon)
        </a>
    </div>

    <!-- Main Dashboard Content -->
    <div>
        <!-- Quick Stats -->
        <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 2rem;">
            <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
                <div style="font-size: 2rem; font-weight: bold; color: #764ba2;">0</div>
                <div>Potential Matches</div>
            </div>
            <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
                <div style="font-size: 2rem; font-weight: bold; color: #764ba2;">0</div>
                <div>Messages</div>
            </div>
            <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
                <div style="font-size: 2rem; font-weight: bold; color: #764ba2;">0</div>
                <div>Profile Views</div>
            </div>
        </div>

        <!-- Action Cards -->
        <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem;">

            <!-- Find Matches Card -->
            <div style="background: white; padding: 1.5rem; border-radius: 10px; border: 1px solid #e9ecef;">
                <h4 style="margin-top: 0; color: #764ba2;">🔍 Find Matches</h4>
                <p>Discover people who might actually respond to your messages.</p>
                <a href="#" class="btn" style="width: 100%; text-align: center; background: #17a2b8;">
                    Start Swiping (Coming in Part 3)
                </a>
            </div>

            <!-- Messages Card -->
            <div style="background: white; padding: 1.5rem; border-radius: 10px; border: 1px solid #e9ecef;">
                <h4 style="margin-top: 0; color: #764ba2;">💌 Messages</h4>
                <p>Check your messages. Or don't. We're not judging.</p>
                <a href="#" class="btn" style="width: 100%; text-align: center; background: #28a745;">
                    View Messages (Part 4)
                </a>
            </div>

            <!-- Profile Card -->
            <div style="background: white; padding: 1.5rem; border-radius: 10px; border: 1px solid #e9ecef;">
                <h4 style="margin-top: 0; color: #764ba2;">👤 Your Profile</h4>
                <p>Spruce up your profile. Add better photos than your driver's license.</p>
                <a href="#" class="btn" style="width: 100%; text-align: center; background: #ffc107; color: #000;">
                    Enhance Profile (Part 5)
                </a>
            </div>

            <!-- Settings Card -->
            <div style="background: white; padding: 1.5rem; border-radius: 10px; border: 1px solid #e9ecef;">
                <h4 style="margin-top: 0; color: #764ba2;">⚙️ Settings</h4>
                <p>Configure your preferences. Like setting your dating radius beyond your couch.</p>
                <a href="#" class="btn" style="width: 100%; text-align: center; background: #6c757d;">
                    Settings (Part 6)
                </a>
            </div>
        </div>

        <!-- Quick Tip -->
        <div style="background: #fff3cd; border: 1px solid #ffeaa7; padding: 1rem; border-radius: 8px; margin-top: 2rem;">
            <strong>💡 Pro Tip:</strong> Complete your profile to get 10x more matches! 
            (Or so we tell everyone to make them feel better)
        </div>
    </div>
</div>

 include_once 'includes/footer.php'; 

3. Logout Functionality

Create logout.php - the graceful exit:


// logout.php
require_once 'includes/functions.php';

// Start session if not already started
if (session_status() === PHP_SESSION_NONE) {
    session_start();
}

// Store username for farewell message
$username = $_SESSION['username'] ?? 'Friend';

// Destroy all session data
// This is like burning the evidence after a great party
$_SESSION = array();

// If it's desired to kill the session, also delete the 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"]
    );
}

// Finally, destroy the session
session_destroy();

// Redirect to login with a farewell message
redirect('login.php', "See you soon, $username! Don't be a stranger.");

4. Homepage

Update index.php to be more welcoming:


// index.php
require_once 'includes/functions.php';

// Check if user is logged in
if (is_logged_in()) {
    redirect('dashboard.php');
}

include_once 'includes/header.php';

<div style="text-align: center; padding: 4rem 0;">
    <h1 style="font-size: 3rem; margin-bottom: 1rem;">
        Welcome to MateFinder 🔥
    </h1>
    <p style="font-size: 1.2rem; color: #666; margin-bottom: 2rem;">
        Where meaningful connections happen. Or at least, that's what our marketing team tells us to say.
    </p>

    <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem; margin: 3rem 0;">
        <div style="padding: 2rem;">
            <div style="font-size: 3rem; margin-bottom: 1rem;">👥</div>
            <h3>Real People</h3>
            <p>No robots. Probably. We're like 87% sure.</p>
        </div>
        <div style="padding: 2rem;">
            <div style="font-size: 3rem; margin-bottom: 1rem;">💬</div>
            <h3>Actual Conversations</h3>
            <p>More than just "hey" and then radio silence.</p>
        </div>
        <div style="padding: 2rem;">
            <div style="font-size: 3rem; margin-bottom: 1rem;">🎯</div>
            <h3>Smart Matching</h3>
            <p>Our algorithm is smarter than your last dating decision.</p>
        </div>
    </div>

    <div style="margin-top: 3rem;">
        <a href="register.php" class="btn" style="padding: 1rem 2rem; font-size: 1.2rem; margin-right: 1rem;">
            Find Your Match - It's Free!
        </a>
        <a href="login.php" class="btn" style="padding: 1rem 2rem; font-size: 1.2rem; background: #6c757d;">
            I've Been Here Before
        </a>
    </div>

    <div style="margin-top: 4rem; padding: 2rem; background: #f8f9fa; border-radius: 10px;">
        <h2>How It Works</h2>
        <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-top: 2rem;">
            <div>
                <div style="font-size: 2rem; font-weight: bold; color: #764ba2;">1</div>
                <h4>Sign Up</h4>
                <p>Create your profile. It's easier than assembling IKEA furniture.</p>
            </div>
            <div>
                <div style="font-size: 2rem; font-weight: bold; color: #764ba2;">2</div>
                <h4>Find Matches</h4>
                <p>Browse profiles of people who might actually like you back.</p>
            </div>
            <div>
                <div style="font-size: 2rem; font-weight: bold; color: #764ba2;">3</div>
                <h4>Start Talking</h4>
                <p>Break the ice without breaking a sweat.</p>
            </div>
            <div>
                <div style="font-size: 2rem; font-weight: bold; color: #764ba2;">4</div>
                <h4>Meet Up</h4>
                <p>Take it offline. Remember to wear pants.</p>
            </div>
        </div>
    </div>
</div>

 include_once 'includes/footer.php'; 

5. Update Functions.php

Add this function to includes/functions.php:

// Get user data by ID
// Useful for when you need to remember who someone is
function get_user_data($user_id, $conn) {
    $query = "SELECT id, username, email, first_name, last_name, gender, date_of_birth, 
                     profile_created, last_login, is_active 
              FROM users WHERE id = :id";
    $stmt = $conn->prepare($query);
    $stmt->bindParam(':id', $user_id);
    $stmt->execute();

    if ($stmt->rowCount() === 1) {
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    return false;
}

What We've Accomplished in Part 2

Boom! We now have a fully functional authentication system:

  1. Login System - The velvet rope that lets the right people in
  2. Session Management - Keeping users logged in (unlike your memory after tequila)
  3. Dashboard - A beautiful control center that currently has zero matches (we'll fix that!)
  4. Logout - The graceful exit for when users need to make a quick getaway
  5. Homepage - A welcoming entrance that doesn't scream "I was built in a weekend"

Testing Your Creation

  1. Register a new user - Fill out the form like you're actually looking for love
  2. Login with that user - Experience the joy of successful authentication
  3. Explore the dashboard - Marvel at all the zeros in your stats (for now)
  4. Logout - Feel the sweet release of session destruction
  5. Try to access dashboard without login - Get redirected like a trespasser

What's Broken (On Purpose)

Notice how most buttons say "Coming Soon"? That's because we're building this thing piece by piece, like a LEGO set where you actually read the instructions.

What's Coming in Part 3

In the next installment, we'll tackle:

Current Status: Your dating site now has a working login system! It's like having a club with a bouncer but no music or drinks yet. But hey, the security is tight!


Pro Tip: Always test your logout functionality. It's like knowing where the emergency exits are - you hope you never need them, but when you do, you better hope they work.

Building a Dating Website in PHP - Part 3: Making People Look Good

Welcome back, you coding Casanova! In Parts 1 and 2, we built the foundation and the front door. Now it's time to make people actually want to stick around. We're building user profiles that don't look like they were designed by someone who thinks "romantic" means extra cheese on pizza.

What We're Building in Part 3

1. Database Updates - Level Up!

First, let's enhance our database. Run these SQL commands:

-- Add more profile fields because "male/female/other" is a bit... sparse
ALTER TABLE users ADD COLUMN (
    bio TEXT,
    location VARCHAR(100),
    occupation VARCHAR(100),
    interests TEXT,
    relationship_goal ENUM('dating', 'friendship', 'marriage', 'casual', 'not_sure') DEFAULT 'not_sure',
    height INT COMMENT 'Height in cm',
    profile_photo VARCHAR(255),
    profile_completed TINYINT(1) DEFAULT 0
);

-- Create photos table for those carefully curated selfies
CREATE TABLE user_photos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    photo_path VARCHAR(255) NOT NULL,
    is_profile_photo TINYINT(1) DEFAULT 0,
    upload_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    caption VARCHAR(255),
    display_order INT DEFAULT 0,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_user_id (user_id),
    INDEX idx_display_order (display_order)
);

-- Create a table to track who's checking out who (in a non-creepy way)
CREATE TABLE profile_views (
    id INT AUTO_INCREMENT PRIMARY KEY,
    viewer_id INT NOT NULL,
    viewed_user_id INT NOT NULL,
    view_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (viewer_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (viewed_user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_viewed_user (viewed_user_id),
    INDEX idx_view_date (view_date)
);

2. File Upload Helper

Create includes/file_upload.php - because handling user uploads is like herding cats:


// includes/file_upload.php

function handle_photo_upload($file, $user_id) {
    // Check if file was uploaded without errors
    if ($file['error'] !== UPLOAD_ERR_OK) {
        return ['success' => false, 'error' => 'File upload failed. Did you even select a file?'];
    }

    // Check file size - no 50MB bathroom mirror selfies
    if ($file['size'] > 5 * 1024 * 1024) {
        return ['success' => false, 'error' => 'File is too large. We get it, you have a high-quality camera.'];
    }

    // Check file type - we want images, not your tax returns
    $allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
    $file_type = mime_content_type($file['tmp_name']);

    if (!in_array($file_type, $allowed_types)) {
        return ['success' => false, 'error' => 'Only JPEG, PNG, GIF, and WebP images are allowed.'];
    }

    // Create user upload directory if it doesn't exist
    $upload_dir = "assets/uploads/users/{$user_id}/";
    if (!file_exists($upload_dir)) {
        mkdir($upload_dir, 0755, true);
    }

    // Generate unique filename to avoid conflicts
    $file_extension = pathinfo($file['name'], PATHINFO_EXTENSION);
    $filename = uniqid() . '_' . time() . '.' . $file_extension;
    $file_path = $upload_dir . $filename;

    // Move uploaded file - like telling your photos where to live
    if (move_uploaded_file($file['tmp_name'], $file_path)) {
        return [
            'success' => true, 
            'file_path' => $file_path,
            'filename' => $filename
        ];
    } else {
        return ['success' => false, 'error' => 'Failed to save file. Try again?'];
    }
}

// Function to set profile photo
function set_profile_photo($user_id, $photo_path, $conn) {
    try {
        // First, unset any existing profile photo
        $update_user = "UPDATE users SET profile_photo = :photo_path WHERE id = :user_id";
        $stmt_user = $conn->prepare($update_user);
        $stmt_user->bindParam(':photo_path', $photo_path);
        $stmt_user->bindParam(':user_id', $user_id);
        $stmt_user->execute();

        // Update photos table
        $update_photos = "UPDATE user_photos SET is_profile_photo = 0 WHERE user_id = :user_id";
        $stmt_photos = $conn->prepare($update_photos);
        $stmt_photos->bindParam(':user_id', $user_id);
        $stmt_photos->execute();

        // Set the new profile photo
        $set_profile = "UPDATE user_photos SET is_profile_photo = 1 WHERE user_id = :user_id AND photo_path = :photo_path";
        $stmt_set = $conn->prepare($set_profile);
        $stmt_set->bindParam(':user_id', $user_id);
        $stmt_set->bindParam(':photo_path', $photo_path);
        $stmt_set->execute();

        return true;
    } catch (PDOException $e) {
        error_log("Profile photo update error: " . $e->getMessage());
        return false;
    }
}

3. Enhanced User Profile Page

Create profile.php - where users can show off their best angles:


// profile.php
require_once 'config/database.php';
require_once 'includes/functions.php';

if (!is_logged_in()) {
    redirect('login.php', "Login to view profiles. We promise it's worth it!");
}

$database = new Database();
$conn = $database->getConnection();

// Check if we're viewing own profile or someone else's
$viewing_own_profile = false;
$profile_user_id = $_GET['user_id'] ?? $_SESSION['user_id'];

if ($profile_user_id == $_SESSION['user_id']) {
    $viewing_own_profile = true;
}

// Get profile user data
$query = "SELECT u.*, 
          COUNT(DISTINCT pv.id) as view_count,
          COUNT(DISTINCT up.id) as photo_count
          FROM users u 
          LEFT JOIN profile_views pv ON u.id = pv.viewed_user_id 
          LEFT JOIN user_photos up ON u.id = up.user_id 
          WHERE u.id = :user_id 
          GROUP BY u.id";
$stmt = $conn->prepare($query);
$stmt->bindParam(':user_id', $profile_user_id);
$stmt->execute();

if ($stmt->rowCount() === 0) {
    redirect('dashboard.php', "User not found. Maybe they ghosted us too?");
}

$user = $stmt->fetch(PDO::FETCH_ASSOC);

// If viewing someone else's profile, record the view
if (!$viewing_own_profile) {
    $view_query = "INSERT INTO profile_views (viewer_id, viewed_user_id) VALUES (:viewer_id, :viewed_user_id)";
    $view_stmt = $conn->prepare($view_query);
    $view_stmt->bindParam(':viewer_id', $_SESSION['user_id']);
    $view_stmt->bindParam(':viewed_user_id', $profile_user_id);
    $view_stmt->execute();
}

// Get user photos
$photos_query = "SELECT * FROM user_photos WHERE user_id = :user_id ORDER BY is_profile_photo DESC, display_order ASC";
$photos_stmt = $conn->prepare($photos_query);
$photos_stmt->bindParam(':user_id', $profile_user_id);
$photos_stmt->execute();
$photos = $photos_stmt->fetchAll(PDO::FETCH_ASSOC);

// Calculate age
$birth_date = new DateTime($user['date_of_birth']);
$today = new DateTime();
$age = $today->diff($birth_date)->y;

// Calculate profile completion percentage
$completion_fields = ['username', 'email', 'first_name', 'last_name', 'gender', 'date_of_birth', 'bio', 'location', 'occupation'];
$completed_fields = 0;
foreach ($completion_fields as $field) {
    if (!empty($user[$field])) {
        $completed_fields++;
    }
}
$completion_percentage = round(($completed_fields / count($completion_fields)) * 100);

include_once 'includes/header.php';

<h1>
     echo $user['first_name'] . ' ' . $user['last_name']; 
     if ($viewing_own_profile): 
        <small style="font-size: 1rem; color: #666;">(That's you, handsome!)</small>
     endif; 
</h1>

<!-- Profile Completion Alert -->
 if ($viewing_own_profile && $completion_percentage < 80): 
<div style="background: #fff3cd; border: 1px solid #ffeaa7; padding: 1rem; border-radius: 8px; margin-bottom: 2rem;">
    <h4 style="margin-top: 0;">🚧 Your Profile Needs Some Love</h4>
    <p>Complete your profile to get more matches! Currently at  echo $completion_percentage; %</p>
    <div style="background: #e9ecef; border-radius: 10px; height: 10px; margin: 0.5rem 0;">
        <div style="background: #ffc107; height: 100%; border-radius: 10px; width:  echo $completion_percentage; %;"></div>
    </div>
    <a href="edit_profile.php" class="btn" style="background: #17a2b8;">Complete My Profile</a>
</div>
 endif; 

<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 2rem;">

    <!-- Left Column - Photos and Basic Info -->
    <div>
        <!-- Profile Photo -->
        <div style="background: white; padding: 1.5rem; border-radius: 10px; margin-bottom: 1.5rem; text-align: center;">
             if (!empty($user['profile_photo'])): 
                <img src=" echo $user['profile_photo']; " 
                     alt=" echo $user['first_name']; " 
                     style="width: 200px; height: 200px; border-radius: 50%; object-fit: cover; border: 3px solid #764ba2;">
             else: 
                <div style="width: 200px; height: 200px; border-radius: 50%; background: #e9ecef; display: flex; align-items: center; justify-content: center; margin: 0 auto; border: 3px solid #764ba2;">
                    <span style="font-size: 3rem;">👤</span>
                </div>
             endif; 

            <h3 style="margin: 1rem 0 0.5rem 0;"> echo $user['first_name']; ,  echo $age; </h3>
            <p style="color: #666; margin: 0;"> echo $user['location'] ?? 'Location not set'; </p>

             if ($viewing_own_profile): 
                <a href="edit_profile.php" class="btn" style="margin-top: 1rem;">Edit Profile</a>
             else: 
                <div style="margin-top: 1rem;">
                    <button class="btn" style="background: #28a745; margin-right: 0.5rem;">❤️ Like</button>
                    <button class="btn" style="background: #17a2b8;">💌 Message</button>
                </div>
             endif; 
        </div>

        <!-- Quick Stats -->
        <div style="background: white; padding: 1.5rem; border-radius: 10px;">
            <h4 style="margin-top: 0;">Profile Stats</h4>
            <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
                <div style="text-align: center;">
                    <div style="font-size: 1.5rem; font-weight: bold; color: #764ba2;"> echo $user['photo_count']; </div>
                    <div style="font-size: 0.8rem;">Photos</div>
                </div>
                <div style="text-align: center;">
                    <div style="font-size: 1.5rem; font-weight: bold; color: #764ba2;"> echo $user['view_count']; </div>
                    <div style="font-size: 0.8rem;">Profile Views</div>
                </div>
            </div>
        </div>
    </div>

    <!-- Right Column - Detailed Info -->
    <div>
        <!-- About Section -->
        <div style="background: white; padding: 1.5rem; border-radius: 10px; margin-bottom: 1.5rem;">
            <h3 style="margin-top: 0;">About  echo $user['first_name']; </h3>

             if (!empty($user['bio'])): 
                <p style="line-height: 1.6;"> echo nl2br(htmlspecialchars($user['bio'])); </p>
             else: 
                <p style="color: #999; font-style: italic;">
                     echo $viewing_own_profile ? 'Write something about yourself! People love a good story.' : 'No bio yet.'; 
                </p>
             endif; 

            <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 1.5rem;">
                <div>
                    <strong>📍 Location:</strong><br>
                     echo !empty($user['location']) ? $user['location'] : '<span style="color: #999;">Not specified</span>'; 
                </div>
                <div>
                    <strong>💼 Occupation:</strong><br>
                     echo !empty($user['occupation']) ? $user['occupation'] : '<span style="color: #999;">Not specified</span>'; 
                </div>
                <div>
                    <strong>🎯 Looking For:</strong><br>

                    $goals = [
                        'dating' => 'Serious Dating',
                        'friendship' => 'New Friends',
                        'marriage' => 'Marriage',
                        'casual' => 'Casual Dating',
                        'not_sure' => 'Figuring it out'
                    ];
                    echo $goals[$user['relationship_goal']] ?? 'Not specified';

                </div>
                <div>
                    <strong>📏 Height:</strong><br>

                    if (!empty($user['height'])) {
                        $feet = floor($user['height'] / 30.48);
                        $inches = round(($user['height'] % 30.48) / 2.54);
                        echo "{$user['height']}cm ({$feet}'{$inches}\")";
                    } else {
                        echo '<span style="color: #999;">Not specified</span>';
                    }

                </div>
            </div>

             if (!empty($user['interests'])): 
                <div style="margin-top: 1.5rem;">
                    <strong>🎨 Interests:</strong><br>
                    <div style="margin-top: 0.5rem;">

                        $interests = explode(',', $user['interests']);
                        foreach ($interests as $interest): 
                            $interest = trim($interest);
                            if (!empty($interest)):

                            <span style="background: #f8f9fa; padding: 0.25rem 0.5rem; border-radius: 15px; font-size: 0.8rem; margin: 0.25rem; display: inline-block;">
                                 echo htmlspecialchars($interest); 
                            </span>

                            endif;
                        endforeach; 

                    </div>
                </div>
             endif; 
        </div>

        <!-- Photos Gallery -->
        <div style="background: white; padding: 1.5rem; border-radius: 10px;">
            <h4 style="margin-top: 0;">📸 Photos</h4>

             if (count($photos) > 0): 
                <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem;">
                     foreach ($photos as $photo): 
                        <div style="aspect-ratio: 1; overflow: hidden; border-radius: 8px; position: relative;">
                            <img src=" echo $photo['photo_path']; " 
                                 alt="Photo" 
                                 style="width: 100%; height: 100%; object-fit: cover;">
                             if ($photo['is_profile_photo']): 
                                <div style="position: absolute; top: 0.25rem; right: 0.25rem; background: #764ba2; color: white; padding: 0.25rem 0.5rem; border-radius: 10px; font-size: 0.7rem;">
                                    Main
                                </div>
                             endif; 
                        </div>
                     endforeach; 
                </div>
             else: 
                <p style="color: #999; text-align: center; padding: 2rem;">
                     echo $viewing_own_profile ? 'Add some photos to make your profile pop!' : 'No photos yet.'; 
                </p>
             endif; 

             if ($viewing_own_profile): 
                <div style="text-align: center; margin-top: 1rem;">
                    <a href="edit_profile.php#photos" class="btn">Manage Photos</a>
                </div>
             endif; 
        </div>
    </div>
</div>

 include_once 'includes/footer.php'; 

4. Profile Editing Page

Create edit_profile.php - where users can fix their life choices:


// edit_profile.php
require_once 'config/database.php';
require_once 'includes/functions.php';
require_once 'includes/file_upload.php';

if (!is_logged_in()) {
    redirect('login.php', "Login to edit your profile. We know you want to!");
}

$database = new Database();
$conn = $database->getConnection();

$user_id = $_SESSION['user_id'];
$errors = [];
$success = false;

// Get current user data
$query = "SELECT * FROM users WHERE id = :user_id";
$stmt = $conn->prepare($query);
$stmt->bindParam(':user_id', $user_id);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);

// Handle form submission
if ($_POST) {
    // Sanitize all inputs
    $first_name = sanitize_input($_POST['first_name'] ?? '');
    $last_name = sanitize_input($_POST['last_name'] ?? '');
    $bio = sanitize_input($_POST['bio'] ?? '');
    $location = sanitize_input($_POST['location'] ?? '');
    $occupation = sanitize_input($_POST['occupation'] ?? '');
    $interests = sanitize_input($_POST['interests'] ?? '');
    $relationship_goal = sanitize_input($_POST['relationship_goal'] ?? 'not_sure');
    $height = !empty($_POST['height']) ? (int)$_POST['height'] : null;

    // Basic validation
    if (empty($first_name)) {
        $errors[] = "First name is required. Unless you go by 'Hey You'.";
    }

    if (empty($last_name)) {
        $errors[] = "Last name is required. It's how we know which Smith we're talking about.";
    }

    if (strlen($bio) > 500) {
        $errors[] = "Bio is too long. Save some mystery for the first date!";
    }

    // Handle profile photo upload
    $profile_photo_path = $user['profile_photo'];
    if (!empty($_FILES['profile_photo']['name'])) {
        $upload_result = handle_photo_upload($_FILES['profile_photo'], $user_id);

        if ($upload_result['success']) {
            $profile_photo_path = $upload_result['file_path'];

            // Save to photos table
            $photo_query = "INSERT INTO user_photos (user_id, photo_path, is_profile_photo) 
                           VALUES (:user_id, :photo_path, 1) 
                           ON DUPLICATE KEY UPDATE is_profile_photo = 1";
            $photo_stmt = $conn->prepare($photo_query);
            $photo_stmt->bindParam(':user_id', $user_id);
            $photo_stmt->bindParam(':photo_path', $profile_photo_path);
            $photo_stmt->execute();

            // Update user profile photo
            set_profile_photo($user_id, $profile_photo_path, $conn);
        } else {
            $errors[] = $upload_result['error'];
        }
    }

    // If no errors, update the profile
    if (empty($errors)) {
        $update_query = "UPDATE users SET 
                        first_name = :first_name,
                        last_name = :last_name,
                        bio = :bio,
                        location = :location,
                        occupation = :occupation,
                        interests = :interests,
                        relationship_goal = :relationship_goal,
                        height = :height,
                        profile_photo = :profile_photo,
                        profile_completed = 1
                        WHERE id = :user_id";

        $update_stmt = $conn->prepare($update_query);
        $update_stmt->bindParam(':first_name', $first_name);
        $update_stmt->bindParam(':last_name', $last_name);
        $update_stmt->bindParam(':bio', $bio);
        $update_stmt->bindParam(':location', $location);
        $update_stmt->bindParam(':occupation', $occupation);
        $update_stmt->bindParam(':interests', $interests);
        $update_stmt->bindParam(':relationship_goal', $relationship_goal);
        $update_stmt->bindParam(':height', $height);
        $update_stmt->bindParam(':profile_photo', $profile_photo_path);
        $update_stmt->bindParam(':user_id', $user_id);

        if ($update_stmt->execute()) {
            $_SESSION['first_name'] = $first_name;
            $success = true;
            // Refresh user data
            $stmt->execute();
            $user = $stmt->fetch(PDO::FETCH_ASSOC);
        } else {
            $errors[] = "Failed to update profile. Our servers are having a bad day.";
        }
    }
}

// Get user photos
$photos_query = "SELECT * FROM user_photos WHERE user_id = :user_id ORDER BY is_profile_photo DESC, display_order ASC";
$photos_stmt = $conn->prepare($photos_query);
$photos_stmt->bindParam(':user_id', $user_id);
$photos_stmt->execute();
$photos = $photos_stmt->fetchAll(PDO::FETCH_ASSOC);

include_once 'includes/header.php';

<h1>Edit Your Profile ✏️</h1>
<p>Make yourself look good. Or at least, better than your driver's license photo.</p>

 if ($success): 
    <div style="background: #d4edda; color: #155724; padding: 1rem; border-radius: 5px; margin: 1rem 0;">
        <strong>Success!</strong> Your profile has been updated. Looking good!
    </div>
 endif; 

 if (!empty($errors)): 
    <div style="background: #f8d7da; color: #721c24; padding: 1rem; border-radius: 5px; margin: 1rem 0;">
        <strong>Oops! Some issues:</strong>
        <ul>
             foreach ($errors as $error): 
                <li> echo $error; </li>
             endforeach; 
        </ul>
    </div>
 endif; 

<form method="POST" action="" enctype="multipart/form-data" style="max-width: 800px;">

    <!-- Profile Photo Section -->
    <div style="background: white; padding: 1.5rem; border-radius: 10px; margin-bottom: 1.5rem; text-align: center;">
        <h3 style="margin-top: 0;">🖼️ Profile Photo</h3>

        <div style="margin-bottom: 1rem;">
             if (!empty($user['profile_photo'])): 
                <img src=" echo $user['profile_photo']; " 
                     alt="Current profile photo" 
                     style="width: 150px; height: 150px; border-radius: 50%; object-fit: cover; border: 3px solid #764ba2;">
             else: 
                <div style="width: 150px; height: 150px; border-radius: 50%; background: #e9ecef; display: flex; align-items: center; justify-content: center; margin: 0 auto; border: 3px solid #764ba2;">
                    <span style="font-size: 2rem;">👤</span>
                </div>
             endif; 
        </div>

        <div>
            <label for="profile_photo" class="btn" style="background: #17a2b8; cursor: pointer;">
                📸 Upload New Photo
            </label>
            <input type="file" id="profile_photo" name="profile_photo" 
                   accept="image/*" style="display: none;" onchange="this.form.submit()">
            <p style="font-size: 0.8rem; color: #666; margin: 0.5rem 0 0 0;">
                JPG, PNG, GIF, or WebP. Max 5MB. Smiling recommended. 😊
            </p>
        </div>
    </div>

    <!-- Basic Information -->
    <div style="background: white; padding: 1.5rem; border-radius: 10px; margin-bottom: 1.5rem;">
        <h3 style="margin-top: 0;">👤 Basic Information</h3>

        <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem;">
            <div>
                <label for="first_name">First Name:</label>
                <input type="text" id="first_name" name="first_name" required 
                       style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;"
                       value=" echo htmlspecialchars($user['first_name']); ">
            </div>
            <div>
                <label for="last_name">Last Name:</label>
                <input type="text" id="last_name" name="last_name" required 
                       style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;"
                       value=" echo htmlspecialchars($user['last_name']); ">
            </div>
        </div>

        <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
            <div>
                <label for="location">📍 Location:</label>
                <input type="text" id="location" name="location" 
                       style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;"
                       value=" echo htmlspecialchars($user['location'] ?? ''); "
                       placeholder="City, State">
            </div>
            <div>
                <label for="occupation">💼 Occupation:</label>
                <input type="text" id="occupation" name="occupation" 
                       style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;"
                       value=" echo htmlspecialchars($user['occupation'] ?? ''); "
                       placeholder="What do you do?">
            </div>
        </div>
    </div>

    <!-- About You -->
    <div style="background: white; padding: 1.5rem; border-radius: 10px; margin-bottom: 1.5rem;">
        <h3 style="margin-top: 0;">📝 About You</h3>

        <div style="margin-bottom: 1rem;">
            <label for="bio">Bio (500 characters max):</label>
            <textarea id="bio" name="bio" rows="4" 
                      style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;"
                      placeholder="Tell people about yourself. Funny is good. Desperate is... not."> echo htmlspecialchars($user['bio'] ?? ''); </textarea>
            <div style="text-align: right; font-size: 0.8rem; color: #666;">
                <span id="bio_counter">0</span>/500 characters
            </div>
        </div>

        <div style="margin-bottom: 1rem;">
            <label for="interests">🎨 Interests (comma separated):</label>
            <input type="text" id="interests" name="interests" 
                   style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;"
                   value=" echo htmlspecialchars($user['interests'] ?? ''); "
                   placeholder="e.g., hiking, cooking, complaining about the weather">
        </div>
    </div>

    <!-- Dating Preferences -->
    <div style="background: white; padding: 1.5rem; border-radius: 10px; margin-bottom: 1.5rem;">
        <h3 style="margin-top: 0;">💑 Dating Preferences</h3>

        <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
            <div>
                <label for="relationship_goal">🎯 What are you looking for?</label>
                <select id="relationship_goal" name="relationship_goal" 
                        style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;">
                    <option value="not_sure"  echo ($user['relationship_goal'] ?? 'not_sure') === 'not_sure' ? 'selected' : ''; >Figuring it out</option>
                    <option value="friendship"  echo ($user['relationship_goal'] ?? '') === 'friendship' ? 'selected' : ''; >New Friends</option>
                    <option value="casual"  echo ($user['relationship_goal'] ?? '') === 'casual' ? 'selected' : ''; >Casual Dating</option>
                    <option value="dating"  echo ($user['relationship_goal'] ?? '') === 'dating' ? 'selected' : ''; >Serious Dating</option>
                    <option value="marriage"  echo ($user['relationship_goal'] ?? '') === 'marriage' ? 'selected' : ''; >Marriage</option>
                </select>
            </div>
            <div>
                <label for="height">📏 Height (cm):</label>
                <input type="number" id="height" name="height" min="100" max="250" 
                       style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;"
                       value=" echo $user['height'] ?? ''; "
                       placeholder="Height in centimeters">
            </div>
        </div>
    </div>

    <button type="submit" class="btn" style="width: 100%; padding: 1rem; font-size: 1.1rem;">
        💾 Save Profile Updates
    </button>
</form>

<script>
// Bio character counter
document.getElementById('bio').addEventListener('input', function() {
    const counter = document.getElementById('bio_counter');
    counter.textContent = this.value.length;
});

// Set initial counter value
document.getElementById('bio_counter').textContent = document.getElementById('bio').value.length;
</script>

 include_once 'includes/footer.php'; 

5. Update Dashboard Links

Update the dashboard in dashboard.php to include links to the new profile pages:

<!-- In the Action Cards section, update the Profile card -->
<div style="background: white; padding: 1.5rem; border-radius: 10px; border: 1px solid #e9ecef;">
    <h4 style="margin-top: 0; color: #764ba2;">👤 Your Profile</h4>
    <p>Spruce up your profile. Add better photos than your driver's license.</p>
    <a href="profile.php" class="btn" style="width: 100%; text-align: center; background: #17a2b8; margin-bottom: 0.5rem;">
        View My Profile
    </a>
    <a href="edit_profile.php" class="btn" style="width: 100%; text-align: center; background: #ffc107; color: #000;">
        Edit Profile
    </a>
</div>

What We've Accomplished in Part 3

Boom! We've transformed our basic dating site into something that actually looks like a dating site:

  1. Enhanced User Profiles - Now with 100% more personality!
  2. Profile Editing - Letting users fix their questionable life choices
  3. Photo Upload System - For those carefully curated selfies
  4. Rich Profile Data - Bio, interests, occupation, and more
  5. Profile Completion Tracking - Guilt-tripping users into filling out their profiles

Testing Your Enhanced Creation

  1. Edit your profile - Add a bio, interests, and upload a photo
  2. View your profile - Marvel at your digital transformation
  3. Check profile completion - Watch that percentage climb
  4. Test photo uploads - Make sure it handles your best angles

What's Still Coming

Current Status: Your dating site now has actual profiles! It's like having a club where people actually have personalities instead of just standing around looking awkward.


Pro Tip: Always validate file uploads. The internet is full of people who think uploading their entire photo library is a good idea. It's not.

Building a Dating Website in PHP - Part 4: The Digital Cupid Strikes!

Welcome back, you matching-making maestro! In Part 3, we made people look good. Now it's time to play God (or at least, digital cupid). We're building the matching system - because what's a dating site without the potential for awkward first dates?

What We're Building in Part 4

1. Database Updates - The Match Factory

First, let's build our match-making machinery. Run these SQL commands:

-- Table for tracking likes (the digital equivalent of awkward eye contact)
CREATE TABLE user_likes (
    id INT AUTO_INCREMENT PRIMARY KEY,
    liker_id INT NOT NULL,
    liked_id INT NOT NULL,
    like_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    super_like TINYINT(1) DEFAULT 0 COMMENT 'For when you REALLY like someone',
    FOREIGN KEY (liker_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (liked_id) REFERENCES users(id) ON DELETE CASCADE,
    UNIQUE KEY unique_like (liker_id, liked_id),
    INDEX idx_liked_id (liked_id),
    INDEX idx_like_date (like_date)
);

-- Table for actual matches (when the feeling is mutual)
CREATE TABLE user_matches (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user1_id INT NOT NULL,
    user2_id INT NOT NULL,
    match_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_interaction TIMESTAMP NULL,
    is_active TINYINT(1) DEFAULT 1,
    FOREIGN KEY (user1_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (user2_id) REFERENCES users(id) ON DELETE CASCADE,
    UNIQUE KEY unique_match (user1_id, user2_id),
    INDEX idx_user1 (user1_id),
    INDEX idx_user2 (user2_id),
    INDEX idx_match_date (match_date)
);

-- Table for user preferences (what people think they want)
CREATE TABLE user_preferences (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    min_age INT DEFAULT 18,
    max_age INT DEFAULT 99,
    preferred_gender ENUM('male', 'female', 'any') DEFAULT 'any',
    max_distance INT DEFAULT 50 COMMENT 'Maximum distance in miles',
    has_photos TINYINT(1) DEFAULT 1 COMMENT 'Only show users with photos',
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    UNIQUE KEY unique_user_preferences (user_id)
);

-- Insert default preferences for existing users
INSERT INTO user_preferences (user_id)
SELECT id FROM users;

2. Matching Engine Helper

Create includes/matching_engine.php - our digital cupid:


// includes/matching_engine.php

// Get potential matches for a user
function get_potential_matches($user_id, $limit = 20, $conn) {
    // First, get user's own data and preferences
    $user_query = "SELECT u.*, up.preferred_gender, up.min_age, up.max_age 
                   FROM users u 
                   LEFT JOIN user_preferences up ON u.id = up.user_id 
                   WHERE u.id = :user_id";
    $user_stmt = $conn->prepare($user_query);
    $user_stmt->bindParam(':user_id', $user_id);
    $user_stmt->execute();
    $user = $user_stmt->fetch(PDO::FETCH_ASSOC);

    // Calculate user's age
    $birth_date = new DateTime($user['date_of_birth']);
    $today = new DateTime();
    $user_age = $today->diff($birth_date)->y;

    // Build the query for potential matches
    $query = "SELECT u.*, 
              TIMESTAMPDIFF(YEAR, u.date_of_birth, CURDATE()) as age,
              (SELECT COUNT(*) FROM user_photos up WHERE up.user_id = u.id) as photo_count
              FROM users u 
              WHERE u.id != :user_id 
              AND u.is_active = 1 
              AND u.profile_completed = 1";

    $params = [':user_id' => $user_id];

    // Add gender preference
    if ($user['preferred_gender'] !== 'any') {
        $query .= " AND u.gender = :preferred_gender";
        $params[':preferred_gender'] = $user['preferred_gender'];
    }

    // Add age range
    $query .= " AND TIMESTAMPDIFF(YEAR, u.date_of_birth, CURDATE()) BETWEEN :min_age AND :max_age";
    $params[':min_age'] = $user['min_age'] ?? 18;
    $params[':max_age'] = $user['max_age'] ?? 99;

    // Only show users with photos if preferred
    if ($user['has_photos'] ?? 1) {
        $query .= " AND (SELECT COUNT(*) FROM user_photos up WHERE up.user_id = u.id) > 0";
    }

    // Exclude already liked/passed users
    $query .= " AND u.id NOT IN (
        SELECT liked_id FROM user_likes WHERE liker_id = :user_id
    )";

    // Add some randomness to make it interesting
    $query .= " ORDER BY RAND() LIMIT :limit";
    $params[':limit'] = $limit;

    $stmt = $conn->prepare($query);

    // Bind parameters
    foreach ($params as $key => $value) {
        if ($key === ':limit') {
            $stmt->bindValue($key, $value, PDO::PARAM_INT);
        } else {
            $stmt->bindValue($key, $value);
        }
    }

    $stmt->execute();
    $matches = $stmt->fetchAll(PDO::FETCH_ASSOC);

    // Calculate compatibility score for each match
    foreach ($matches as &$match) {
        $match['compatibility_score'] = calculate_compatibility($user, $match, $conn);
    }

    // Sort by compatibility score (highest first)
    usort($matches, function($a, $b) {
        return $b['compatibility_score'] - $a['compatibility_score'];
    });

    return $matches;
}

// Calculate compatibility score between two users
function calculate_compatibility($user1, $user2, $conn) {
    $score = 50; // Start with a neutral score

    // Age compatibility (closer age = higher score)
    $age_diff = abs($user1['age'] - $user2['age']);
    if ($age_diff <= 2) $score += 15;
    elseif ($age_diff <= 5) $score += 10;
    elseif ($age_diff <= 10) $score += 5;

    // Relationship goal compatibility
    if ($user1['relationship_goal'] === $user2['relationship_goal']) {
        $score += 20;
    }

    // Interests compatibility
    if (!empty($user1['interests']) && !empty($user2['interests'])) {
        $user1_interests = array_map('trim', explode(',', $user1['interests']));
        $user2_interests = array_map('trim', explode(',', $user2['interests']));
        $common_interests = array_intersect($user1_interests, $user2_interests);

        $interest_score = min(count($common_interests) * 5, 15);
        $score += $interest_score;
    }

    // Location compatibility (if we had real locations, we'd use distance)
    if (!empty($user1['location']) && !empty($user2['location'])) {
        if ($user1['location'] === $user2['location']) {
            $score += 10;
        }
    }

    // Profile completeness bonus
    $user2_completeness = calculate_profile_completeness($user2);
    $score += ($user2_completeness / 100) * 5;

    // Ensure score is between 0 and 100
    return max(0, min(100, $score));
}

// Calculate profile completeness percentage
function calculate_profile_completeness($user) {
    $fields = ['bio', 'location', 'occupation', 'interests', 'relationship_goal', 'height'];
    $completed = 0;

    foreach ($fields as $field) {
        if (!empty($user[$field])) {
            $completed++;
        }
    }

    return ($completed / count($fields)) * 100;
}

// Handle user like
function handle_user_like($liker_id, $liked_id, $is_super_like = false, $conn) {
    try {
        // Check if it's a match (the other user already liked this user)
        $check_match = "SELECT id FROM user_likes 
                       WHERE liker_id = :liked_id AND liked_id = :liker_id";
        $check_stmt = $conn->prepare($check_match);
        $check_stmt->bindParam(':liked_id', $liked_id);
        $check_stmt->bindParam(':liker_id', $liker_id);
        $check_stmt->execute();

        $is_match = $check_stmt->rowCount() > 0;

        // Record the like
        $like_query = "INSERT INTO user_likes (liker_id, liked_id, super_like) 
                      VALUES (:liker_id, :liked_id, :super_like)";
        $like_stmt = $conn->prepare($like_query);
        $like_stmt->bindParam(':liker_id', $liker_id);
        $like_stmt->bindParam(':liked_id', $liked_id);
        $like_stmt->bindParam(':super_like', $is_super_like, PDO::PARAM_BOOL);
        $like_stmt->execute();

        // If it's a match, create a match record
        if ($is_match) {
            // Ensure user1_id is always the smaller ID to avoid duplicates
            $user1_id = min($liker_id, $liked_id);
            $user2_id = max($liker_id, $liked_id);

            $match_query = "INSERT INTO user_matches (user1_id, user2_id) 
                           VALUES (:user1_id, :user2_id)";
            $match_stmt = $conn->prepare($match_query);
            $match_stmt->bindParam(':user1_id', $user1_id);
            $match_stmt->bindParam(':user2_id', $user2_id);
            $match_stmt->execute();

            return ['success' => true, 'is_match' => true, 'match_id' => $conn->lastInsertId()];
        }

        return ['success' => true, 'is_match' => false];

    } catch (PDOException $e) {
        error_log("Like error: " . $e->getMessage());
        return ['success' => false, 'error' => 'Failed to process like'];
    }
}

// Get user's matches
function get_user_matches($user_id, $conn) {
    $query = "SELECT m.*, 
              CASE 
                  WHEN m.user1_id = :user_id THEN u2.id
                  ELSE u1.id
              END as match_user_id,
              CASE 
                  WHEN m.user1_id = :user_id THEN u2.first_name
                  ELSE u1.first_name
              END as match_first_name,
              CASE 
                  WHEN m.user1_id = :user_id THEN u2.last_name
                  ELSE u1.last_name
              END as match_last_name,
              CASE 
                  WHEN m.user1_id = :user_id THEN u2.profile_photo
                  ELSE u1.profile_photo
              END as match_profile_photo,
              CASE 
                  WHEN m.user1_id = :user_id THEN u2.gender
                  ELSE u1.gender
              END as match_gender
              FROM user_matches m
              JOIN users u1 ON m.user1_id = u1.id
              JOIN users u2 ON m.user2_id = u2.id
              WHERE (m.user1_id = :user_id OR m.user2_id = :user_id)
              AND m.is_active = 1
              ORDER BY m.last_interaction DESC, m.match_date DESC";

    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->execute();

    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

// Get like history
function get_like_history($user_id, $conn) {
    $query = "SELECT ul.*, u.first_name, u.last_name, u.profile_photo, u.gender
              FROM user_likes ul
              JOIN users u ON ul.liked_id = u.id
              WHERE ul.liker_id = :user_id
              ORDER BY ul.like_date DESC
              LIMIT 50";

    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->execute();

    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

3. Discover Page - Where Magic Happens

Create discover.php - the digital meat market:


// discover.php
require_once 'config/database.php';
require_once 'includes/functions.php';
require_once 'includes/matching_engine.php';

if (!is_logged_in()) {
    redirect('login.php', "Login to discover amazing people. Your future self will thank you!");
}

$database = new Database();
$conn = $database->getConnection();

$user_id = $_SESSION['user_id'];
$message = '';

// Handle like/pass actions
if ($_POST && isset($_POST['action']) && isset($_POST['user_id'])) {
    $target_user_id = (int)$_POST['user_id'];
    $action = $_POST['action'];

    if ($action === 'like') {
        $is_super_like = isset($_POST['super_like']);
        $result = handle_user_like($user_id, $target_user_id, $is_super_like, $conn);

        if ($result['success']) {
            if ($result['is_match']) {
                $message = "🎉 It's a match! You both like each other!";
            } else {
                $message = "❤️ Like sent! Hope they feel the same way...";
            }
        } else {
            $message = "❌ Failed to send like. Try again?";
        }
    } elseif ($action === 'pass') {
        // Record pass by inserting a "like" with a special flag or just don't show again
        // For now, we'll just not show them again (handled in get_potential_matches)
        $message = "👋 Passed. On to the next!";
    }

    // Use JavaScript to show message and reload after a delay
    echo "<script>
        alert('$message');
        setTimeout(function() { window.location.href = 'discover.php'; }, 500);
    </script>";
    exit();
}

// Get potential matches
$potential_matches = get_potential_matches($user_id, 10, $conn);

include_once 'includes/header.php';

<h1>Discover Amazing People 🔥</h1>
<p>Swipe right on people who catch your eye. Or left if they remind you of your ex.</p>

 if (empty($potential_matches)): 
    <div style="text-align: center; padding: 4rem 2rem; background: white; border-radius: 10px;">
        <div style="font-size: 4rem; margin-bottom: 1rem;">😴</div>
        <h3>Out of Potential Matches!</h3>
        <p>We've shown you everyone in your area. Either broaden your preferences or move to a more populated area.</p>
        <div style="margin-top: 2rem;">
            <a href="preferences.php" class="btn" style="margin-right: 1rem;">Adjust Preferences</a>
            <a href="matches.php" class="btn" style="background: #17a2b8;">View Your Matches</a>
        </div>
    </div>
 else: 
    <!-- Current Match Card -->
     $current_match = $potential_matches[0]; 
    <div style="max-width: 400px; margin: 0 auto;">

        <!-- Match Card -->
        <div style="background: white; border-radius: 15px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.1);">

            <!-- Photo -->
            <div style="position: relative; height: 400px; background: #f8f9fa;">
                 if (!empty($current_match['profile_photo'])): 
                    <img src=" echo $current_match['profile_photo']; " 
                         alt=" echo $current_match['first_name']; " 
                         style="width: 100%; height: 100%; object-fit: cover;">
                 else: 
                    <div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #667eea, #764ba2);">
                        <span style="font-size: 4rem; color: white;">👤</span>
                    </div>
                 endif; 

                <!-- Compatibility Badge -->
                <div style="position: absolute; top: 1rem; right: 1rem; background: rgba(255,255,255,0.9); padding: 0.5rem 1rem; border-radius: 20px; font-weight: bold; color: #764ba2;">
                     echo $current_match['compatibility_score']; % Match
                </div>

                <!-- Super Like Indicator -->
                 if ($current_match['compatibility_score'] > 85): 
                    <div style="position: absolute; top: 1rem; left: 1rem; background: #17a2b8; color: white; padding: 0.5rem 1rem; border-radius: 20px; font-weight: bold;">
                        💎 High Match!
                    </div>
                 endif; 
            </div>

            <!-- Info -->
            <div style="padding: 1.5rem;">
                <div style="display: flex; justify-content: between; align-items: center; margin-bottom: 1rem;">
                    <h2 style="margin: 0; font-size: 1.5rem;">
                         echo $current_match['first_name']; ,  echo $current_match['age']; 
                    </h2>
                    <span style="font-size: 1.5rem;">
                         echo $current_match['gender'] === 'male' ? '♂️' : '♀️'; 
                    </span>
                </div>

                 if (!empty($current_match['occupation'])): 
                    <p style="margin: 0.5rem 0; color: #666;">
                        💼  echo $current_match['occupation']; 
                    </p>
                 endif; 

                 if (!empty($current_match['location'])): 
                    <p style="margin: 0.5rem 0; color: #666;">
                        📍  echo $current_match['location']; 
                    </p>
                 endif; 

                 if (!empty($current_match['bio'])): 
                    <p style="margin: 1rem 0; line-height: 1.5;">
                         echo nl2br(htmlspecialchars(substr($current_match['bio'], 0, 150) . (strlen($current_match['bio']) > 150 ? '...' : ''))); 
                    </p>
                 endif; 

                <!-- Interests -->
                 if (!empty($current_match['interests'])): 
                    <div style="margin: 1rem 0;">
                        <strong>🎨 Interests:</strong>
                        <div style="margin-top: 0.5rem;">

                            $interests = explode(',', $current_match['interests']);
                            foreach (array_slice($interests, 0, 5) as $interest): 
                                $interest = trim($interest);
                                if (!empty($interest)):

                                <span style="background: #f8f9fa; padding: 0.25rem 0.5rem; border-radius: 15px; font-size: 0.8rem; margin: 0.25rem; display: inline-block;">
                                     echo htmlspecialchars($interest); 
                                </span>

                                endif;
                            endforeach; 

                        </div>
                    </div>
                 endif; 
            </div>
        </div>

        <!-- Action Buttons -->
        <div style="display: flex; justify-content: center; gap: 2rem; margin-top: 2rem;">
            <!-- Pass Button -->
            <form method="POST" action="" style="margin: 0;">
                <input type="hidden" name="action" value="pass">
                <input type="hidden" name="user_id" value=" echo $current_match['id']; ">
                <button type="submit" 
                        style="width: 70px; height: 70px; border-radius: 50%; border: 3px solid #dc3545; background: white; color: #dc3545; font-size: 2rem; cursor: pointer; transition: all 0.3s;"
                        onmouseover="this.style.background='#dc3545'; this.style.color='white'"
                        onmouseout="this.style.background='white'; this.style.color='#dc3545'">
                    ✖️
                </button>
            </form>

            <!-- Super Like Button -->
            <form method="POST" action="" style="margin: 0;">
                <input type="hidden" name="action" value="like">
                <input type="hidden" name="user_id" value=" echo $current_match['id']; ">
                <input type="hidden" name="super_like" value="1">
                <button type="submit" 
                        style="width: 70px; height: 70px; border-radius: 50%; border: 3px solid #17a2b8; background: white; color: #17a2b8; font-size: 2rem; cursor: pointer; transition: all 0.3s;"
                        onmouseover="this.style.background='#17a2b8'; this.style.color='white'"
                        onmouseout="this.style.background='white'; this.style.color='#17a2b8'">
                    💎
                </button>
            </form>

            <!-- Like Button -->
            <form method="POST" action="" style="margin: 0;">
                <input type="hidden" name="action" value="like">
                <input type="hidden" name="user_id" value=" echo $current_match['id']; ">
                <button type="submit" 
                        style="width: 70px; height: 70px; border-radius: 50%; border: 3px solid #28a745; background: white; color: #28a745; font-size: 2rem; cursor: pointer; transition: all 0.3s;"
                        onmouseover="this.style.background='#28a745'; this.style.color='white'"
                        onmouseout="this.style.background='white'; this.style.color='#28a745'">
                    ❤️
                </button>
            </form>
        </div>

        <!-- Quick Stats -->
        <div style="text-align: center; margin-top: 2rem; color: #666;">
            <p> echo count($potential_matches);  more potential matches waiting</p>
        </div>
    </div>
 endif; 

<!-- How It Works Section -->
<div style="background: #f8f9fa; padding: 2rem; border-radius: 10px; margin-top: 3rem;">
    <h3 style="text-align: center; margin-bottom: 2rem;">How This Works</h3>
    <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem; text-align: center;">
        <div>
            <div style="font-size: 2rem; margin-bottom: 1rem;">❤️</div>
            <h4>Like</h4>
            <p>Send a like when you're interested. It's the digital equivalent of smiling from across the room.</p>
        </div>
        <div>
            <div style="font-size: 2rem; margin-bottom: 1rem;">💎</div>
            <h4>Super Like</h4>
            <p>For when you REALLY like someone. Use sparingly - like your last clean shirt.</p>
        </div>
        <div>
            <div style="font-size: 2rem; margin-bottom: 1rem;">✖️</div>
            <h4>Pass</h4>
            <p>Not your type? No worries. There's plenty of fish in the digital sea.</p>
        </div>
    </div>
</div>

 include_once 'includes/footer.php'; 

4. Matches Page

Create matches.php - where mutual admiration is displayed:


// matches.php
require_once 'config/database.php';
require_once 'includes/functions.php';
require_once 'includes/matching_engine.php';

if (!is_logged_in()) {
    redirect('login.php', "Login to see your matches. The suspense is killing us!");
}

$database = new Database();
$conn = $database->getConnection();

$user_id = $_SESSION['user_id'];
$matches = get_user_matches($user_id, $conn);

include_once 'includes/header.php';

<h1>Your Matches 💕</h1>
<p>These are the people who actually like you back. Try not to mess it up.</p>

 if (empty($matches)): 
    <div style="text-align: center; padding: 4rem 2rem; background: white; border-radius: 10px;">
        <div style="font-size: 4rem; margin-bottom: 1rem;">😢</div>
        <h3>No Matches Yet</h3>
        <p>Keep swiping! Your perfect match is out there, probably also wondering why they have no matches.</p>
        <div style="margin-top: 2rem;">
            <a href="discover.php" class="btn">Start Discovering</a>
        </div>
    </div>
 else: 
    <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.5rem; margin-top: 2rem;">
         foreach ($matches as $match): 
            <div style="background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 5px 15px rgba(0,0,0,0.1);">

                <!-- Match Header -->
                <div style="position: relative; height: 200px; background: linear-gradient(135deg, #667eea, #764ba2);">
                     if (!empty($match['match_profile_photo'])): 
                        <img src=" echo $match['match_profile_photo']; " 
                             alt=" echo $match['match_first_name']; " 
                             style="width: 100%; height: 100%; object-fit: cover;">
                     else: 
                        <div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;">
                            <span style="font-size: 4rem; color: white;">👤</span>
                        </div>
                     endif; 

                    <!-- Match Badge -->
                    <div style="position: absolute; top: 1rem; right: 1rem; background: rgba(255,255,255,0.9); padding: 0.5rem 1rem; border-radius: 20px; font-weight: bold; color: #764ba2;">
                        💖 Match!
                    </div>
                </div>

                <!-- Match Info -->
                <div style="padding: 1.5rem;">
                    <h3 style="margin: 0 0 0.5rem 0;">
                         echo $match['match_first_name'] . ' ' . $match['match_last_name']; 
                    </h3>

                    <p style="color: #666; margin: 0.5rem 0;">
                        Matched on  echo date('F j, Y', strtotime($match['match_date'])); 
                    </p>

                    <div style="display: flex; gap: 0.5rem; margin-top: 1rem;">
                        <a href="profile.php?user_id= echo $match['match_user_id']; " 
                           class="btn" 
                           style="flex: 1; text-align: center; background: #17a2b8;">
                            View Profile
                        </a>
                        <button class="btn" 
                                style="flex: 1; text-align: center; background: #28a745;"
                                onclick="alert('Messaging coming in Part 5!')">
                            💌 Message
                        </button>
                    </div>
                </div>
            </div>
         endforeach; 
    </div>
 endif; 

<!-- Match Statistics -->
 if (!empty($matches)): 
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 10px; margin-top: 2rem;">
    <h3>Your Match Stats 📊</h3>
    <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; text-align: center;">
        <div style="background: white; padding: 1rem; border-radius: 8px;">
            <div style="font-size: 1.5rem; font-weight: bold; color: #764ba2;"> echo count($matches); </div>
            <div>Total Matches</div>
        </div>
        <div style="background: white; padding: 1rem; border-radius: 8px;">
            <div style="font-size: 1.5rem; font-weight: bold; color: #764ba2;">

                $this_week = array_filter($matches, function($match) {
                    return strtotime($match['match_date']) >= strtotime('-7 days');
                });
                echo count($this_week);

            </div>
            <div>This Week</div>
        </div>
        <div style="background: white; padding: 1rem; border-radius: 8px;">
            <div style="font-size: 1.5rem; font-weight: bold; color: #764ba2;">

                $female_matches = array_filter($matches, function($match) {
                    return $match['match_gender'] === 'female';
                });
                echo count($female_matches);

            </div>
            <div>Female Matches</div>
        </div>
        <div style="background: white; padding: 1rem; border-radius: 8px;">
            <div style="font-size: 1.5rem; font-weight: bold; color: #764ba2;">

                $male_matches = array_filter($matches, function($match) {
                    return $match['match_gender'] === 'male';
                });
                echo count($male_matches);

            </div>
            <div>Male Matches</div>
        </div>
    </div>
</div>
 endif; 

 include_once 'includes/footer.php'; 

5. Update Dashboard

Update dashboard.php to show match statistics:

<!-- Replace the Quick Stats section in dashboard.php -->
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 2rem;">
    <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
        <div style="font-size: 2rem; font-weight: bold; color: #764ba2;">

            $match_count = count(get_user_matches($_SESSION['user_id'], $conn));
            echo $match_count;

        </div>
        <div>Matches</div>
    </div>
    <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
        <div style="font-size: 2rem; font-weight: bold; color: #764ba2;">

            $like_history = get_like_history($_SESSION['user_id'], $conn);
            echo count($like_history);

        </div>
        <div>Likes Sent</div>
    </div>
    <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
        <div style="font-size: 2rem; font-weight: bold; color: #764ba2;">0</div>
        <div>Messages</div>
    </div>
    <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
        <div style="font-size: 2rem; font-weight: bold; color: #764ba2;"> echo $user['view_count'] ?? 0; </div>
        <div>Profile Views</div>
    </div>
</div>

What We've Accomplished in Part 4

Holy match-making, Batman! We've built:

  1. Matching Algorithm - Our digital cupid is in business!
  2. Like/Pass System - Swipe right functionality without the carpal tunnel
  3. Matches Page - Where mutual admiration is celebrated
  4. Compatibility Scoring - Because "you both like pizza" is a solid foundation
  5. Super Likes - For when you REALLY like someone's profile picture

Testing Your Match-Making Machine

  1. Register multiple test users - Create some variety in your dating pool
  2. Use the discover page - Swipe right, left, and super like to your heart's content
  3. Check matches - See the magic happen when likes are mutual
  4. Test compatibility scoring - See if the algorithm agrees with your taste

What's Still Coming

Current Status: Your dating site now has actual matching! It's like having a wingman who never gets tired and doesn't drink all your beer.


Pro Tip: The matching algorithm is basic. In a real dating site, you'd use machine learning, but for now, "you both like dogs" is a solid start. Remember: even the most sophisticated algorithms can't explain why someone would swipe left on a photo with a puppy.

Building a Dating Website in PHP - Part 5: Let's Talk! (Messaging System)

Welcome back, you chat-loving champion! In Part 4, we made matches happen. Now it's time to make those matches actually talk to each other. Because what's the point of matching if you can't send that awkward "hey" message?

What We're Building in Part 5

1. Database Updates - Message Central

First, let's build our messaging infrastructure. Run these SQL commands:

-- Conversations table (because we need to organize this chaos)
CREATE TABLE conversations (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user1_id INT NOT NULL,
    user2_id INT NOT NULL,
    last_message_id INT NULL,
    last_message_time TIMESTAMP NULL,
    user1_deleted TINYINT(1) DEFAULT 0,
    user2_deleted TINYINT(1) DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user1_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (user2_id) REFERENCES users(id) ON DELETE CASCADE,
    UNIQUE KEY unique_conversation (user1_id, user2_id),
    INDEX idx_user1 (user1_id),
    INDEX idx_user2 (user2_id),
    INDEX idx_last_message (last_message_time)
);

-- Messages table (where the magic happens)
CREATE TABLE messages (
    id INT AUTO_INCREMENT PRIMARY KEY,
    conversation_id INT NOT NULL,
    sender_id INT NOT NULL,
    message_text TEXT NOT NULL,
    message_type ENUM('text', 'image', 'gif') DEFAULT 'text',
    attachment_path VARCHAR(255) NULL,
    is_read TINYINT(1) DEFAULT 0,
    read_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE,
    FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_conversation (conversation_id),
    INDEX idx_sender (sender_id),
    INDEX idx_created (created_at),
    INDEX idx_read_status (is_read)
);

-- Message notifications table (for that dopamine hit)
CREATE TABLE message_notifications (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    conversation_id INT NOT NULL,
    message_id INT NOT NULL,
    is_read TINYINT(1) DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE,
    FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE,
    INDEX idx_user (user_id),
    INDEX idx_unread (user_id, is_read)
);

2. Messaging Engine

Create includes/messaging_engine.php - our digital post office:


// includes/messaging_engine.php

// Get or create conversation between two users
function get_conversation($user1_id, $user2_id, $conn) {
    // Ensure user1_id is always smaller to avoid duplicate conversations
    $user1 = min($user1_id, $user2_id);
    $user2 = max($user1_id, $user2_id);

    $query = "SELECT * FROM conversations 
              WHERE user1_id = :user1_id AND user2_id = :user2_id";
    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user1_id', $user1);
    $stmt->bindParam(':user2_id', $user2);
    $stmt->execute();

    if ($stmt->rowCount() > 0) {
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    // Create new conversation
    $insert_query = "INSERT INTO conversations (user1_id, user2_id) 
                     VALUES (:user1_id, :user2_id)";
    $insert_stmt = $conn->prepare($insert_query);
    $insert_stmt->bindParam(':user1_id', $user1);
    $insert_stmt->bindParam(':user2_id', $user2);
    $insert_stmt->execute();

    return [
        'id' => $conn->lastInsertId(),
        'user1_id' => $user1,
        'user2_id' => $user2,
        'created_at' => date('Y-m-d H:i:s')
    ];
}

// Send a message
function send_message($sender_id, $receiver_id, $message_text, $message_type = 'text', $attachment_path = null, $conn) {
    try {
        // Get conversation
        $conversation = get_conversation($sender_id, $receiver_id, $conn);
        $conversation_id = $conversation['id'];

        // Insert message
        $message_query = "INSERT INTO messages 
                         (conversation_id, sender_id, message_text, message_type, attachment_path) 
                         VALUES (:conversation_id, :sender_id, :message_text, :message_type, :attachment_path)";
        $message_stmt = $conn->prepare($message_query);
        $message_stmt->bindParam(':conversation_id', $conversation_id);
        $message_stmt->bindParam(':sender_id', $sender_id);
        $message_stmt->bindParam(':message_text', $message_text);
        $message_stmt->bindParam(':message_type', $message_type);
        $message_stmt->bindParam(':attachment_path', $attachment_path);
        $message_stmt->execute();

        $message_id = $conn->lastInsertId();

        // Update conversation last message
        $update_conversation = "UPDATE conversations 
                               SET last_message_id = :message_id, 
                                   last_message_time = NOW() 
                               WHERE id = :conversation_id";
        $update_stmt = $conn->prepare($update_conversation);
        $update_stmt->bindParam(':message_id', $message_id);
        $update_stmt->bindParam(':conversation_id', $conversation_id);
        $update_stmt->execute();

        // Create notification for receiver
        create_message_notification($receiver_id, $conversation_id, $message_id, $conn);

        return [
            'success' => true,
            'message_id' => $message_id,
            'conversation_id' => $conversation_id
        ];

    } catch (PDOException $e) {
        error_log("Message send error: " . $e->getMessage());
        return ['success' => false, 'error' => 'Failed to send message'];
    }
}

// Create message notification
function create_message_notification($user_id, $conversation_id, $message_id, $conn) {
    $query = "INSERT INTO message_notifications (user_id, conversation_id, message_id) 
              VALUES (:user_id, :conversation_id, :message_id)";
    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->bindParam(':conversation_id', $conversation_id);
    $stmt->bindParam(':message_id', $message_id);
    $stmt->execute();
}

// Get user's conversations
function get_user_conversations($user_id, $conn, $limit = 50) {
    $query = "SELECT c.*, 
              u1.first_name as user1_first_name, u1.last_name as user1_last_name, u1.profile_photo as user1_photo,
              u2.first_name as user2_first_name, u2.last_name as user2_last_name, u2.profile_photo as user2_photo,
              m.message_text as last_message, m.created_at as last_message_time, m.sender_id as last_sender_id,
              (SELECT COUNT(*) FROM messages m2 
               WHERE m2.conversation_id = c.id AND m2.is_read = 0 AND m2.sender_id != :user_id) as unread_count
              FROM conversations c
              JOIN users u1 ON c.user1_id = u1.id
              JOIN users u2 ON c.user2_id = u2.id
              LEFT JOIN messages m ON c.last_message_id = m.id
              WHERE (c.user1_id = :user_id OR c.user2_id = :user_id)
              AND (c.user1_deleted = 0 OR c.user1_id != :user_id)
              AND (c.user2_deleted = 0 OR c.user2_id != :user_id)
              ORDER BY c.last_message_time DESC
              LIMIT :limit";

    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
    $stmt->execute();

    $conversations = $stmt->fetchAll(PDO::FETCH_ASSOC);

    // Format conversation data
    foreach ($conversations as &$conv) {
        $conv['other_user'] = ($conv['user1_id'] == $user_id) ? 
            [
                'id' => $conv['user2_id'],
                'first_name' => $conv['user2_first_name'],
                'last_name' => $conv['user2_last_name'],
                'profile_photo' => $conv['user2_photo']
            ] : [
                'id' => $conv['user1_id'],
                'first_name' => $conv['user1_first_name'],
                'last_name' => $conv['user1_last_name'],
                'profile_photo' => $conv['user1_photo']
            ];

        // Truncate last message if too long
        if ($conv['last_message'] && strlen($conv['last_message']) > 50) {
            $conv['last_message'] = substr($conv['last_message'], 0, 50) . '...';
        }
    }

    return $conversations;
}

// Get messages for a conversation
function get_conversation_messages($conversation_id, $user_id, $limit = 50, $offset = 0, $conn) {
    // Mark messages as read when viewing
    mark_messages_as_read($conversation_id, $user_id, $conn);

    $query = "SELECT m.*, u.first_name, u.last_name, u.profile_photo,
              CASE 
                  WHEN m.sender_id = :user_id THEN 'sent'
                  ELSE 'received'
              END as message_direction
              FROM messages m
              JOIN users u ON m.sender_id = u.id
              WHERE m.conversation_id = :conversation_id
              ORDER BY m.created_at DESC
              LIMIT :limit OFFSET :offset";

    $stmt = $conn->prepare($query);
    $stmt->bindParam(':conversation_id', $conversation_id);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
    $stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
    $stmt->execute();

    $messages = $stmt->fetchAll(PDO::FETCH_ASSOC);

    // Reverse to show oldest first
    return array_reverse($messages);
}

// Mark messages as read
function mark_messages_as_read($conversation_id, $user_id, $conn) {
    $query = "UPDATE messages m
              JOIN conversations c ON m.conversation_id = c.id
              SET m.is_read = 1, m.read_at = NOW()
              WHERE m.conversation_id = :conversation_id 
              AND m.sender_id != :user_id
              AND m.is_read = 0";

    $stmt = $conn->prepare($query);
    $stmt->bindParam(':conversation_id', $conversation_id);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->execute();

    // Clear notifications
    $clear_notifications = "UPDATE message_notifications 
                           SET is_read = 1 
                           WHERE user_id = :user_id 
                           AND conversation_id = :conversation_id";
    $clear_stmt = $conn->prepare($clear_notifications);
    $clear_stmt->bindParam(':user_id', $user_id);
    $clear_stmt->bindParam(':conversation_id', $conversation_id);
    $clear_stmt->execute();
}

// Get unread message count for user
function get_unread_message_count($user_id, $conn) {
    $query = "SELECT COUNT(*) as unread_count
              FROM messages m
              JOIN conversations c ON m.conversation_id = c.id
              WHERE ((c.user1_id = :user_id AND c.user1_deleted = 0) 
                     OR (c.user2_id = :user_id AND c.user2_deleted = 0))
              AND m.sender_id != :user_id
              AND m.is_read = 0";

    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->execute();

    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    return $result['unread_count'] ?? 0;
}

// Delete conversation for user (soft delete)
function delete_conversation_for_user($conversation_id, $user_id, $conn) {
    $query = "UPDATE conversations 
              SET user1_deleted = IF(user1_id = :user_id, 1, user1_deleted),
                  user2_deleted = IF(user2_id = :user_id, 1, user2_deleted)
              WHERE id = :conversation_id";

    $stmt = $conn->prepare($query);
    $stmt->bindParam(':conversation_id', $conversation_id);
    $stmt->bindParam(':user_id', $user_id);
    return $stmt->execute();
}

3. Messages Inbox

Create messages.php - where all the conversations live:


// messages.php
require_once 'config/database.php';
require_once 'includes/functions.php';
require_once 'includes/messaging_engine.php';

if (!is_logged_in()) {
    redirect('login.php', "Login to see your messages. People might be waiting!");
}

$database = new Database();
$conn = $database->getConnection();

$user_id = $_SESSION['user_id'];
$conversations = get_user_conversations($user_id, $conn);
$unread_count = get_unread_message_count($user_id, $conn);

// Handle conversation deletion
if (isset($_POST['delete_conversation'])) {
    $conversation_id = (int)$_POST['conversation_id'];
    if (delete_conversation_for_user($conversation_id, $user_id, $conn)) {
        $_SESSION['flash_message'] = "Conversation deleted. Out of sight, out of mind!";
        redirect('messages.php');
    }
}

include_once 'includes/header.php';

<h1>Your Messages 💌</h1>
<p>This is where the magic happens. Or where you get ghosted. Either way, it's exciting!</p>

<!-- Quick Stats -->
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin: 2rem 0;">
    <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
        <div style="font-size: 1.5rem; font-weight: bold; color: #764ba2;"> echo count($conversations); </div>
        <div>Conversations</div>
    </div>
    <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
        <div style="font-size: 1.5rem; font-weight: bold; color: #dc3545;"> echo $unread_count; </div>
        <div>Unread Messages</div>
    </div>
    <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
        <div style="font-size: 1.5rem; font-weight: bold; color: #764ba2;">

            $active_conv = array_filter($conversations, function($conv) {
                return strtotime($conv['last_message_time']) >= strtotime('-7 days');
            });
            echo count($active_conv);

        </div>
        <div>Active This Week</div>
    </div>
    <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
        <div style="font-size: 1.5rem; font-weight: bold; color: #764ba2;">

            $matches = get_user_matches($user_id, $conn);
            echo count($matches);

        </div>
        <div>Total Matches</div>
    </div>
</div>

<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 2rem; min-height: 600px;">

    <!-- Conversations List -->
    <div style="background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 5px 15px rgba(0,0,0,0.1);">
        <div style="padding: 1.5rem; border-bottom: 1px solid #eee;">
            <h3 style="margin: 0;">Conversations</h3>
        </div>

        <div style="max-height: 500px; overflow-y: auto;">
             if (empty($conversations)): 
                <div style="text-align: center; padding: 3rem 2rem; color: #666;">
                    <div style="font-size: 3rem; margin-bottom: 1rem;">💬</div>
                    <h4>No Conversations Yet</h4>
                    <p>Match with someone and start chatting!</p>
                    <a href="discover.php" class="btn" style="margin-top: 1rem;">Find Matches</a>
                </div>
             else: 
                 foreach ($conversations as $conv): 
                    <a href="conversation.php?user_id= echo $conv['other_user']['id']; " 
                       style="display: block; padding: 1rem 1.5rem; border-bottom: 1px solid #f8f9fa; text-decoration: none; color: inherit; transition: background 0.3s;"
                       onmouseover="this.style.background='#f8f9fa'"
                       onmouseout="this.style.background='white'">

                        <div style="display: flex; align-items: center; gap: 1rem;">
                            <!-- Profile Photo -->
                            <div style="position: relative;">
                                 if (!empty($conv['other_user']['profile_photo'])): 
                                    <img src=" echo $conv['other_user']['profile_photo']; " 
                                         alt=" echo $conv['other_user']['first_name']; " 
                                         style="width: 50px; height: 50px; border-radius: 50%; object-fit: cover;">
                                 else: 
                                    <div style="width: 50px; height: 50px; border-radius: 50%; background: #e9ecef; display: flex; align-items: center; justify-content: center;">
                                        <span style="font-size: 1.2rem;">👤</span>
                                    </div>
                                 endif; 

                                <!-- Online Status Indicator -->
                                <div style="position: absolute; bottom: 2px; right: 2px; width: 12px; height: 12px; background: #28a745; border: 2px solid white; border-radius: 50%;"></div>
                            </div>

                            <!-- Conversation Info -->
                            <div style="flex: 1; min-width: 0;">
                                <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem;">
                                    <h4 style="margin: 0; font-size: 1rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
                                         echo $conv['other_user']['first_name'] . ' ' . $conv['other_user']['last_name']; 
                                    </h4>
                                    <small style="color: #666; font-size: 0.8rem;">

                                        if ($conv['last_message_time']) {
                                            echo time_ago($conv['last_message_time']);
                                        }

                                    </small>
                                </div>

                                <p style="margin: 0; color: #666; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
                                     if ($conv['last_message']): 
                                         echo htmlspecialchars($conv['last_message']); 
                                     else: 
                                        <em>No messages yet</em>
                                     endif; 
                                </p>
                            </div>

                            <!-- Unread Badge -->
                             if ($conv['unread_count'] > 0): 
                                <div style="background: #dc3545; color: white; border-radius: 50%; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: bold;">
                                     echo $conv['unread_count']; 
                                </div>
                             endif; 
                        </div>
                    </a>
                 endforeach; 
             endif; 
        </div>
    </div>

    <!-- Selected Conversation / Welcome Message -->
    <div style="background: white; border-radius: 10px; padding: 2rem; box-shadow: 0 5px 15px rgba(0,0,0,0.1); display: flex; align-items: center; justify-content: center; text-align: center;">
        <div>
            <div style="font-size: 4rem; margin-bottom: 1rem;">💬</div>
            <h3>Select a Conversation</h3>
            <p style="color: #666; margin-bottom: 2rem;">
                Choose a conversation from the list to start messaging,<br>
                or go find someone new to chat with!
            </p>
            <div style="display: flex; gap: 1rem; justify-content: center;">
                <a href="discover.php" class="btn">Discover People</a>
                <a href="matches.php" class="btn" style="background: #17a2b8;">View Matches</a>
            </div>
        </div>
    </div>
</div>

// Helper function for time ago
function time_ago($datetime) {
    $time = strtotime($datetime);
    $now = time();
    $diff = $now - $time;

    if ($diff < 60) return 'just now';
    if ($diff < 3600) return floor($diff / 60) . 'm ago';
    if ($diff < 86400) return floor($diff / 3600) . 'h ago';
    if ($diff < 604800) return floor($diff / 86400) . 'd ago';
    return date('M j', $time);
}

 include_once 'includes/footer.php'; 

4. Individual Conversation Page

Create conversation.php - where actual chatting happens:


// conversation.php
require_once 'config/database.php';
require_once 'includes/functions.php';
require_once 'includes/messaging_engine.php';

if (!is_logged_in()) {
    redirect('login.php', "Login to continue your conversation. Don't leave them hanging!");
}

$database = new Database();
$conn = $database->getConnection();

$user_id = $_SESSION['user_id'];
$other_user_id = (int)$_GET['user_id'];

// Check if users are matched
$match_check = "SELECT * FROM user_matches 
               WHERE (user1_id = :user1 AND user2_id = :user2) 
               OR (user1_id = :user2 AND user2_id = :user1)";
$match_stmt = $conn->prepare($match_check);
$match_stmt->bindParam(':user1', $user_id);
$match_stmt->bindParam(':user2', $other_user_id);
$match_stmt->execute();

if ($match_stmt->rowCount() === 0) {
    redirect('matches.php', "You need to match with this user before messaging. No sliding into DMs allowed!");
}

// Get other user info
$user_query = "SELECT * FROM users WHERE id = :user_id";
$user_stmt = $conn->prepare($user_query);
$user_stmt->bindParam(':user_id', $other_user_id);
$user_stmt->execute();
$other_user = $user_stmt->fetch(PDO::FETCH_ASSOC);

if (!$other_user) {
    redirect('matches.php', "User not found. Maybe they ghosted us too?");
}

// Get conversation
$conversation = get_conversation($user_id, $other_user_id, $conn);
$conversation_id = $conversation['id'];

// Handle message sending
if ($_POST && isset($_POST['message_text']) && !empty(trim($_POST['message_text']))) {
    $message_text = trim($_POST['message_text']);

    if (strlen($message_text) > 1000) {
        $error = "Message is too long. Keep it under 1000 characters. We believe in quality over quantity!";
    } else {
        $result = send_message($user_id, $other_user_id, $message_text, 'text', null, $conn);

        if (!$result['success']) {
            $error = "Failed to send message. Try again?";
        } else {
            // Clear the message input
            $_POST['message_text'] = '';
        }
    }
}

// Get messages
$messages = get_conversation_messages($conversation_id, $user_id, 100, 0, $conn);

include_once 'includes/header.php';

<h1>Chat with  echo $other_user['first_name'];  💬</h1>

<div style="display: grid; grid-template-columns: 1fr 3fr; gap: 2rem; min-height: 600px;">

    <!-- User Info Sidebar -->
    <div style="background: white; border-radius: 10px; padding: 1.5rem; box-shadow: 0 5px 15px rgba(0,0,0,0.1); height: fit-content;">
        <div style="text-align: center; margin-bottom: 1.5rem;">
             if (!empty($other_user['profile_photo'])): 
                <img src=" echo $other_user['profile_photo']; " 
                     alt=" echo $other_user['first_name']; " 
                     style="width: 100px; height: 100px; border-radius: 50%; object-fit: cover; margin-bottom: 1rem;">
             else: 
                <div style="width: 100px; height: 100px; border-radius: 50%; background: #e9ecef; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem auto;">
                    <span style="font-size: 2rem;">👤</span>
                </div>
             endif; 

            <h3 style="margin: 0 0 0.5rem 0;"> echo $other_user['first_name'] . ' ' . $other_user['last_name']; </h3>

            $birth_date = new DateTime($other_user['date_of_birth']);
            $today = new DateTime();
            $age = $today->diff($birth_date)->y;

            <p style="color: #666; margin: 0 0 1rem 0;">Age:  echo $age; </p>

             if (!empty($other_user['location'])): 
                <p style="color: #666; margin: 0 0 1rem 0;">📍  echo $other_user['location']; </p>
             endif; 
        </div>

        <div style="border-top: 1px solid #eee; padding-top: 1rem;">
            <h4 style="margin: 0 0 1rem 0;">Quick Actions</h4>
            <div style="display: flex; flex-direction: column; gap: 0.5rem;">
                <a href="profile.php?user_id= echo $other_user_id; " class="btn" style="text-align: center; background: #17a2b8;">
                    View Profile
                </a>
                <form method="POST" action="messages.php" style="margin: 0;">
                    <input type="hidden" name="conversation_id" value=" echo $conversation_id; ">
                    <input type="hidden" name="delete_conversation" value="1">
                    <button type="submit" class="btn" style="width: 100%; background: #dc3545;" 
                            onclick="return confirm('Are you sure you want to delete this conversation?')">
                        Delete Chat
                    </button>
                </form>
            </div>
        </div>

         if (!empty($other_user['bio'])): 
            <div style="border-top: 1px solid #eee; padding-top: 1rem; margin-top: 1rem;">
                <h4 style="margin: 0 0 0.5rem 0;">About</h4>
                <p style="color: #666; font-size: 0.9rem; line-height: 1.4;">
                     echo nl2br(htmlspecialchars($other_user['bio'])); 
                </p>
            </div>
         endif; 
    </div>

    <!-- Chat Area -->
    <div style="background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 5px 15px rgba(0,0,0,0.1); display: flex; flex-direction: column; height: 600px;">

        <!-- Chat Header -->
        <div style="padding: 1rem 1.5rem; border-bottom: 1px solid #eee; background: #f8f9fa;">
            <div style="display: flex; justify-content: between; align-items: center;">
                <h3 style="margin: 0;">Chat with  echo $other_user['first_name']; </h3>
                <div style="display: flex; align-items: center; gap: 0.5rem;">
                    <span style="background: #28a745; width: 8px; height: 8px; border-radius: 50%;"></span>
                    <small style="color: #666;">Online</small>
                </div>
            </div>
        </div>

        <!-- Messages Area -->
        <div id="messages-container" style="flex: 1; padding: 1rem; overflow-y: auto; background: #f8f9fa;">
             if (empty($messages)): 
                <div style="text-align: center; padding: 3rem 2rem; color: #666;">
                    <div style="font-size: 3rem; margin-bottom: 1rem;">💬</div>
                    <h4>No Messages Yet</h4>
                    <p>Send the first message! Don't just say "hey" - be creative!</p>
                </div>
             else: 
                 foreach ($messages as $message): 
                    <div style="margin-bottom: 1rem; display: flex; 
                                justify-content:  echo $message['message_direction'] === 'sent' ? 'flex-end' : 'flex-start'; ;">

                        <div style="max-width: 70%;">
                            <div style="display: flex; align-items: flex-end; gap: 0.5rem;
                                        flex-direction:  echo $message['message_direction'] === 'sent' ? 'row-reverse' : 'row'; ;">

                                <!-- Profile Photo (only for received messages) -->
                                 if ($message['message_direction'] === 'received'): 
                                    <img src=" echo $message['profile_photo'] ?: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTIiIGZpbGw9IiNlOWVjZWYiLz4KPC9zdmc+'; " 
                                         alt=" echo $message['first_name']; " 
                                         style="width: 30px; height: 30px; border-radius: 50%; object-fit: cover; flex-shrink: 0;">
                                 endif; 

                                <!-- Message Bubble -->
                                <div style="background:  echo $message['message_direction'] === 'sent' ? '#764ba2' : 'white'; ;
                                            color:  echo $message['message_direction'] === 'sent' ? 'white' : 'black'; ;
                                            padding: 0.75rem 1rem; border-radius: 18px; 
                                            border:  echo $message['message_direction'] === 'sent' ? 'none' : '1px solid #e9ecef'; ;
                                            box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
                                    <p style="margin: 0; line-height: 1.4;"> echo nl2br(htmlspecialchars($message['message_text'])); </p>
                                </div>
                            </div>

                            <!-- Message Time -->
                            <div style="text-align:  echo $message['message_direction'] === 'sent' ? 'right' : 'left'; ; 
                                        padding: 0.25rem 0.5rem; font-size: 0.8rem; color: #666;">
                                 echo date('g:i A', strtotime($message['created_at'])); 

                                 if ($message['message_direction'] === 'sent'): 
                                     if ($message['is_read']): 
                                        <span title="Read at  echo date('g:i A', strtotime($message['read_at'])); ">✓✓</span>
                                     else: 
                                        <span title="Delivered">✓</span>
                                     endif; 
                                 endif; 
                            </div>
                        </div>
                    </div>
                 endforeach; 
             endif; 
        </div>

        <!-- Message Input -->
        <div style="padding: 1rem 1.5rem; border-top: 1px solid #eee; background: white;">
             if (isset($error)): 
                <div style="background: #f8d7da; color: #721c24; padding: 0.5rem 1rem; border-radius: 5px; margin-bottom: 1rem;">
                     echo $error; 
                </div>
             endif; 

            <form method="POST" action="" id="message-form">
                <div style="display: flex; gap: 0.5rem;">
                    <input type="text" name="message_text" 
                           placeholder="Type your message here... (Pro tip: Don't just say 'hey')" 
                           style="flex: 1; padding: 0.75rem 1rem; border: 1px solid #ddd; border-radius: 25px; outline: none;"
                           value=" echo $_POST['message_text'] ?? ''; "
                           maxlength="1000">

                    <button type="submit" class="btn" style="border-radius: 25px; padding: 0.75rem 1.5rem;">
                        Send 💌
                    </button>
                </div>
            </form>

            <!-- Quick Message Suggestions -->
            <div style="display: flex; gap: 0.5rem; margin-top: 0.5rem; flex-wrap: wrap;">
                <small style="color: #666;">Quick messages:</small>
                <button type="button" class="quick-message" style="background: none; border: 1px solid #ddd; border-radius: 15px; padding: 0.25rem 0.5rem; font-size: 0.8rem; color: #666; cursor: pointer;"
                        onclick="document.querySelector('[name=message_text]').value = 'Hey, how\\'s your day going? 😊'">
                    How's your day?
                </button>
                <button type="button" class="quick-message" style="background: none; border: 1px solid #ddd; border-radius: 15px; padding: 0.25rem 0.5rem; font-size: 0.8rem; color: #666; cursor: pointer;"
                        onclick="document.querySelector('[name=message_text]').value = 'I noticed we both like  echo explode(',', $other_user['interests'])[0] ?? 'similar things'; ! 🎉'">
                    Common interest
                </button>
                <button type="button" class="quick-message" style="background: none; border: 1px solid #ddd; border-radius: 15px; padding: 0.25rem 0.5rem; font-size: 0.8rem; color: #666; cursor: pointer;"
                        onclick="document.querySelector('[name=message_text]').value = 'Would you like to grab coffee sometime? ☕'">
                    Coffee invite
                </button>
            </div>
        </div>
    </div>
</div>

<script>
// Auto-scroll to bottom of messages
function scrollToBottom() {
    const container = document.getElementById('messages-container');
    container.scrollTop = container.scrollHeight;
}

// Scroll to bottom on page load
document.addEventListener('DOMContentLoaded', function() {
    scrollToBottom();
});

// Auto-refresh messages every 10 seconds
setInterval(function() {
    // In a real app, you'd use AJAX to fetch new messages
    // For now, we'll just reload the page
    // window.location.reload();
}, 10000);

// Focus on message input
document.querySelector('[name="message_text"]').focus();
</script>

 include_once 'includes/footer.php'; 

5. Update Header for Message Notifications

Update includes/header.php to show message notifications:

<!-- In the navigation section, update the nav-links -->
<div class="nav-links">
     if(isset($_SESSION['user_id'])): 
        <a href="discover.php">Discover</a>
        <a href="matches.php">Matches</a>
        <a href="messages.php" style="position: relative;">
            Messages

            $unread_count = get_unread_message_count($_SESSION['user_id'], $conn);
            if ($unread_count > 0): 
                <span style="position: absolute; top: -8px; right: -8px; background: #dc3545; color: white; border-radius: 50%; width: 18px; height: 18px; display: flex; align-items: center; justify-content: center; font-size: 0.7rem;">
                     echo $unread_count; 
                </span>
             endif; 
        </a>
        <a href="dashboard.php">Dashboard</a>
        <a href="logout.php" class="btn">Logout</a>
     else: 
        <a href="login.php">Login</a>
        <a href="register.php" class="btn">Find Your Match</a>
     endif; 
</div>

What We've Accomplished in Part 5

Boom! We've built a full messaging system:

  1. Real-time(ish) Messaging - Conversations that actually work!
  2. Message Inbox - Organized conversations with unread counts
  3. Read Receipts - So you know when you've been left on read
  4. Message Notifications - That sweet dopamine hit
  5. Quick Message Suggestions - For when you're not feeling creative

Testing Your Chat System

  1. Match with test users - Create some connections
  2. Send messages - Test the conversation flow
  3. Check read receipts - See when messages are read
  4. Test notifications - Watch those unread counts update
  5. Try quick messages - Save yourself from typing "hey" for the 100th time

What's Still Coming

Current Status: Your dating site now has actual conversations! It's like building a bar and finally getting people to talk to each other instead of just staring at their drinks.


Pro Tip: In a production environment, you'd use WebSockets for real-time messaging. For now, our "refresh and pray" method works. Remember: the most important feature of any dating app messaging system is the "undo send" button. Unfortunately, we haven't built that yet. Choose your words wisely!

Building a Dating Website in PHP - Part 6: Finding Your Needle in the Haystack

Welcome back, you search-savvy superstar! In Part 5, we built the messaging system. Now it's time to help people actually find what they're looking for. Because scrolling through endless profiles is like trying to find a specific grain of sand on a beach - frustrating and potentially sunburn-inducing.

What We're Building in Part 6

1. Database Updates - Search Central

First, let's enhance our database for better searching. Run these SQL commands:

-- Enhance user_preferences table with more search criteria
ALTER TABLE user_preferences 
ADD COLUMN (
    min_height INT DEFAULT 140 COMMENT 'Minimum height in cm',
    max_height INT DEFAULT 200 COMMENT 'Maximum height in cm',
    education_level ENUM('high_school', 'bachelors', 'masters', 'phd', 'other') DEFAULT NULL,
    has_children ENUM('no', 'yes', 'open') DEFAULT 'open',
    smoking_preference ENUM('no', 'yes', 'sometimes', 'open') DEFAULT 'open',
    drinking_preference ENUM('no', 'yes', 'socially', 'open') DEFAULT 'open',
    religion VARCHAR(50) DEFAULT NULL,
    ethnicity VARCHAR(50) DEFAULT NULL
);

-- Create saved searches table
CREATE TABLE saved_searches (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    search_name VARCHAR(100) NOT NULL,
    search_criteria JSON NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_run TIMESTAMP NULL,
    is_active TINYINT(1) DEFAULT 1,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_user_id (user_id)
);

-- Add indexes for better search performance
ALTER TABLE users ADD INDEX idx_birthdate (date_of_birth);
ALTER TABLE users ADD INDEX idx_location (location);
ALTER TABLE users ADD INDEX idx_created (profile_created);
ALTER TABLE users ADD INDEX idx_active_completed (is_active, profile_completed);

-- Create user_attributes table for additional searchable fields
CREATE TABLE user_attributes (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    attribute_key VARCHAR(50) NOT NULL,
    attribute_value VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    UNIQUE KEY unique_user_attribute (user_id, attribute_key),
    INDEX idx_attribute_key (attribute_key),
    INDEX idx_attribute_value (attribute_value)
);

-- Insert some common attributes
INSERT INTO user_attributes (user_id, attribute_key, attribute_value) VALUES
(1, 'education', 'bachelors'),
(1, 'has_children', 'no'),
(1, 'smoking', 'no'),
(1, 'drinking', 'socially'),
(1, 'religion', 'None'),
(1, 'ethnicity', 'Caucasian');

2. Search Engine Helper

Create includes/search_engine.php - our digital matchmaker's brain:


// includes/search_engine.php

// Main search function
function search_users($search_params, $current_user_id, $conn, $limit = 50, $offset = 0) {
    $query = "SELECT DISTINCT u.*, 
              TIMESTAMPDIFF(YEAR, u.date_of_birth, CURDATE()) as age,
              (SELECT COUNT(*) FROM user_photos up WHERE up.user_id = u.id) as photo_count,
              (SELECT COUNT(*) FROM user_likes ul WHERE ul.liker_id = :current_user_id AND ul.liked_id = u.id) as already_liked
              FROM users u
              LEFT JOIN user_attributes ua ON u.id = ua.user_id
              WHERE u.id != :current_user_id 
              AND u.is_active = 1 
              AND u.profile_completed = 1";

    $params = [':current_user_id' => $current_user_id];

    // Age range
    if (!empty($search_params['min_age']) || !empty($search_params['max_age'])) {
        $min_age = $search_params['min_age'] ?? 18;
        $max_age = $search_params['max_age'] ?? 99;
        $query .= " AND TIMESTAMPDIFF(YEAR, u.date_of_birth, CURDATE()) BETWEEN :min_age AND :max_age";
        $params[':min_age'] = $min_age;
        $params[':max_age'] = $max_age;
    }

    // Gender
    if (!empty($search_params['gender']) && $search_params['gender'] !== 'any') {
        $query .= " AND u.gender = :gender";
        $params[':gender'] = $search_params['gender'];
    }

    // Location (basic - would use geolocation in real app)
    if (!empty($search_params['location'])) {
        $query .= " AND u.location LIKE :location";
        $params[':location'] = '%' . $search_params['location'] . '%';
    }

    // Height range
    if (!empty($search_params['min_height']) || !empty($search_params['max_height'])) {
        $min_height = $search_params['min_height'] ?? 140;
        $max_height = $search_params['max_height'] ?? 200;
        $query .= " AND u.height BETWEEN :min_height AND :max_height";
        $params[':min_height'] = $min_height;
        $params[':max_height'] = $max_height;
    }

    // Relationship goal
    if (!empty($search_params['relationship_goal']) && $search_params['relationship_goal'] !== 'any') {
        $query .= " AND u.relationship_goal = :relationship_goal";
        $params[':relationship_goal'] = $search_params['relationship_goal'];
    }

    // Has photos
    if (!empty($search_params['has_photos'])) {
        $query .= " AND (SELECT COUNT(*) FROM user_photos up WHERE up.user_id = u.id) > 0";
    }

    // Online recently (last 7 days)
    if (!empty($search_params['online_recently'])) {
        $query .= " AND u.last_login >= DATE_SUB(NOW(), INTERVAL 7 DAY)";
    }

    // New members (last 30 days)
    if (!empty($search_params['new_members'])) {
        $query .= " AND u.profile_created >= DATE_SUB(NOW(), INTERVAL 30 DAY)";
    }

    // Keyword search in bio and interests
    if (!empty($search_params['keywords'])) {
        $keywords = explode(' ', $search_params['keywords']);
        $keyword_conditions = [];
        $keyword_index = 0;

        foreach ($keywords as $keyword) {
            if (strlen(trim($keyword)) > 2) { // Only search for words longer than 2 chars
                $param_name = ':keyword_' . $keyword_index;
                $keyword_conditions[] = "(u.bio LIKE $param_name OR u.interests LIKE $param_name)";
                $params[$param_name] = '%' . trim($keyword) . '%';
                $keyword_index++;
            }
        }

        if (!empty($keyword_conditions)) {
            $query .= " AND (" . implode(' OR ', $keyword_conditions) . ")";
        }
    }

    // Advanced attributes
    $attribute_conditions = [];
    $attribute_index = 0;

    $attribute_filters = [
        'education' => 'education',
        'has_children' => 'has_children',
        'smoking' => 'smoking',
        'drinking' => 'drinking',
        'religion' => 'religion',
        'ethnicity' => 'ethnicity'
    ];

    foreach ($attribute_filters as $param_key => $attribute_key) {
        if (!empty($search_params[$param_key]) && $search_params[$param_key] !== 'any') {
            $param_name = ':attr_' . $attribute_index;
            $attribute_conditions[] = "(ua.attribute_key = '$attribute_key' AND ua.attribute_value = $param_name)";
            $params[$param_name] = $search_params[$param_key];
            $attribute_index++;
        }
    }

    if (!empty($attribute_conditions)) {
        $query .= " AND (" . implode(' OR ', $attribute_conditions) . ")";
    }

    // Exclude already liked users
    if (!empty($search_params['exclude_liked'])) {
        $query .= " AND u.id NOT IN (SELECT liked_id FROM user_likes WHERE liker_id = :current_user_id)";
    }

    // Ordering
    $order_by = "u.profile_created DESC"; // default
    if (!empty($search_params['sort_by'])) {
        switch ($search_params['sort_by']) {
            case 'newest':
                $order_by = "u.profile_created DESC";
                break;
            case 'oldest':
                $order_by = "u.profile_created ASC";
                break;
            case 'last_active':
                $order_by = "u.last_login DESC";
                break;
            case 'distance':
                // Would use geolocation in real app
                $order_by = "u.profile_created DESC";
                break;
            case 'compatibility':
                // Simple compatibility based on shared interests
                $order_by = "(SELECT COUNT(*) FROM user_attributes ua2 WHERE ua2.user_id = u.id AND ua2.attribute_value IN (
                    SELECT attribute_value FROM user_attributes WHERE user_id = :current_user_id
                )) DESC, u.profile_created DESC";
                break;
        }
    }

    $query .= " ORDER BY $order_by LIMIT :limit OFFSET :offset";
    $params[':limit'] = $limit;
    $params[':offset'] = $offset;

    $stmt = $conn->prepare($query);

    // Bind parameters
    foreach ($params as $key => $value) {
        if ($key === ':limit' || $key === ':offset') {
            $stmt->bindValue($key, $value, PDO::PARAM_INT);
        } else {
            $stmt->bindValue($key, $value);
        }
    }

    $stmt->execute();
    $results = $stmt->fetchAll(PDO::FETCH_ASSOC);

    // Calculate total count for pagination
    $count_query = "SELECT COUNT(DISTINCT u.id) as total 
                   FROM users u
                   LEFT JOIN user_attributes ua ON u.id = ua.user_id
                   WHERE " . substr($query, strpos($query, 'WHERE') + 6);
    $count_query = preg_replace('/LIMIT.*$/', '', $count_query);

    $count_stmt = $conn->prepare($count_query);

    // Remove limit/offset from params for count query
    $count_params = $params;
    unset($count_params[':limit']);
    unset($count_params[':offset']);

    foreach ($count_params as $key => $value) {
        $count_stmt->bindValue($key, $value);
    }

    $count_stmt->execute();
    $total_count = $count_stmt->fetch(PDO::FETCH_ASSOC)['total'];

    return [
        'results' => $results,
        'total_count' => $total_count,
        'has_more' => ($offset + count($results)) < $total_count
    ];
}

// Save search for user
function save_search($user_id, $search_name, $search_criteria, $conn) {
    $query = "INSERT INTO saved_searches (user_id, search_name, search_criteria) 
              VALUES (:user_id, :search_name, :search_criteria)";
    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->bindParam(':search_name', $search_name);
    $stmt->bindValue(':search_criteria', json_encode($search_criteria));
    return $stmt->execute();
}

// Get user's saved searches
function get_saved_searches($user_id, $conn) {
    $query = "SELECT * FROM saved_searches 
              WHERE user_id = :user_id AND is_active = 1 
              ORDER BY last_run DESC, created_at DESC";
    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->execute();
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

// Update search last run time
function update_search_last_run($search_id, $conn) {
    $query = "UPDATE saved_searches SET last_run = NOW() WHERE id = :search_id";
    $stmt = $conn->prepare($query);
    $stmt->bindParam(':search_id', $search_id);
    return $stmt->execute();
}

// Get search suggestions based on user preferences
function get_search_suggestions($user_id, $conn) {
    $query = "SELECT up.* FROM user_preferences up WHERE up.user_id = :user_id";
    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->execute();
    $preferences = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$preferences) {
        return [];
    }

    $suggestions = [
        'basic' => [
            'name' => 'Basic Match',
            'criteria' => [
                'gender' => $preferences['preferred_gender'],
                'min_age' => $preferences['min_age'],
                'max_age' => $preferences['max_age']
            ]
        ],
        'nearby' => [
            'name' => 'Nearby Members',
            'criteria' => [
                'gender' => $preferences['preferred_gender'],
                'online_recently' => true
            ]
        ],
        'new' => [
            'name' => 'New Members',
            'criteria' => [
                'gender' => $preferences['preferred_gender'],
                'new_members' => true
            ]
        ]
    ];

    return $suggestions;
}

3. Advanced Search Page

Create search.php - where users can find their perfect match:


// search.php
require_once 'config/database.php';
require_once 'includes/functions.php';
require_once 'includes/search_engine.php';

if (!is_logged_in()) {
    redirect('login.php', "Login to search for your perfect match. They're waiting!");
}

$database = new Database();
$conn = $database->getConnection();

$user_id = $_SESSION['user_id'];
$results = [];
$total_count = 0;
$has_more = false;
$search_params = [];
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = 20;
$offset = ($page - 1) * $limit;

// Get search parameters from GET or POST
if ($_GET || $_POST) {
    $search_params = array_merge($_GET, $_POST);

    // Remove pagination parameters
    unset($search_params['page']);

    // Basic sanitization
    foreach ($search_params as $key => $value) {
        if (is_string($value)) {
            $search_params[$key] = sanitize_input($value);
        }
    }

    // Handle saved search
    if (isset($search_params['saved_search_id'])) {
        $saved_search = get_saved_search($search_params['saved_search_id'], $conn);
        if ($saved_search && $saved_search['user_id'] == $user_id) {
            $search_params = array_merge(json_decode($saved_search['search_criteria'], true), $search_params);
            update_search_last_run($search_params['saved_search_id'], $conn);
        }
    }

    // Perform search
    $search_result = search_users($search_params, $user_id, $conn, $limit, $offset);
    $results = $search_result['results'];
    $total_count = $search_result['total_count'];
    $has_more = $search_result['has_more'];
}

// Handle save search
if (isset($_POST['save_search']) && !empty($_POST['search_name'])) {
    if (save_search($user_id, $_POST['search_name'], $search_params, $conn)) {
        $_SESSION['flash_message'] = "Search saved successfully! We'll remember that for you.";
    } else {
        $_SESSION['flash_message'] = "Failed to save search. Try again?";
    }
    redirect('search.php?' . http_build_query($search_params));
}

// Get saved searches and suggestions
$saved_searches = get_saved_searches($user_id, $conn);
$search_suggestions = get_search_suggestions($user_id, $conn);

include_once 'includes/header.php';

<h1>Advanced Search 🔍</h1>
<p>Find your perfect match with our sophisticated search tools. Or just browse until you find someone cute.</p>

<!-- Flash Messages -->
 if (isset($_SESSION['flash_message'])): 
    <div style="background: #d4edda; color: #155724; padding: 1rem; border-radius: 5px; margin: 1rem 0;">
         echo $_SESSION['flash_message']; unset($_SESSION['flash_message']); 
    </div>
 endif; 

<div style="display: grid; grid-template-columns: 300px 1fr; gap: 2rem;">

    <!-- Search Filters Sidebar -->
    <div style="background: white; border-radius: 10px; padding: 1.5rem; box-shadow: 0 5px 15px rgba(0,0,0,0.1); height: fit-content;">
        <form method="GET" action="search.php" id="search-form">

            <!-- Quick Search Suggestions -->
             if (!empty($search_suggestions) && empty($search_params)): 
                <div style="margin-bottom: 1.5rem;">
                    <h4 style="margin: 0 0 1rem 0;">Quick Searches</h4>
                    <div style="display: flex; flex-direction: column; gap: 0.5rem;">
                         foreach ($search_suggestions as $suggestion): 
                            <button type="submit" name="quick_search" value=" echo $suggestion['name']; " 
                                    style="background: none; border: 1px solid #764ba2; color: #764ba2; padding: 0.5rem; border-radius: 5px; cursor: pointer; text-align: left;"
                                    onclick="document.getElementById('search-form').submit();">
                                🔍  echo $suggestion['name']; 
                            </button>
                             foreach ($suggestion['criteria'] as $key => $value): 
                                <input type="hidden" name=" echo $key; " value=" echo $value; ">
                             endforeach; 
                         endforeach; 
                    </div>
                </div>
             endif; 

            <!-- Basic Filters -->
            <div style="margin-bottom: 1.5rem;">
                <h4 style="margin: 0 0 1rem 0;">Basic Criteria</h4>

                <div style="margin-bottom: 1rem;">
                    <label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Gender</label>
                    <select name="gender" style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;">
                        <option value="any"  echo ($search_params['gender'] ?? '') === 'any' ? 'selected' : ''; >Any Gender</option>
                        <option value="male"  echo ($search_params['gender'] ?? '') === 'male' ? 'selected' : ''; >Male</option>
                        <option value="female"  echo ($search_params['gender'] ?? '') === 'female' ? 'selected' : ''; >Female</option>
                    </select>
                </div>

                <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 1rem;">
                    <div>
                        <label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Min Age</label>
                        <input type="number" name="min_age" min="18" max="99" 
                               value=" echo $search_params['min_age'] ?? '18'; "
                               style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;">
                    </div>
                    <div>
                        <label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Max Age</label>
                        <input type="number" name="max_age" min="18" max="99" 
                               value=" echo $search_params['max_age'] ?? '99'; "
                               style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;">
                    </div>
                </div>

                <div style="margin-bottom: 1rem;">
                    <label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Location</label>
                    <input type="text" name="location" 
                           value=" echo $search_params['location'] ?? ''; "
                           placeholder="City, State"
                           style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;">
                </div>
            </div>

            <!-- Advanced Filters -->
            <div style="margin-bottom: 1.5rem;">
                <h4 style="margin: 0 0 1rem 0;">Advanced Filters</h4>

                <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 1rem;">
                    <div>
                        <label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Min Height (cm)</label>
                        <input type="number" name="min_height" min="140" max="200" 
                               value=" echo $search_params['min_height'] ?? ''; "
                               style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;">
                    </div>
                    <div>
                        <label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Max Height (cm)</label>
                        <input type="number" name="max_height" min="140" max="200" 
                               value=" echo $search_params['max_height'] ?? ''; "
                               style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;">
                    </div>
                </div>

                <div style="margin-bottom: 1rem;">
                    <label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Relationship Goal</label>
                    <select name="relationship_goal" style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;">
                        <option value="any"  echo ($search_params['relationship_goal'] ?? '') === 'any' ? 'selected' : ''; >Any Goal</option>
                        <option value="dating"  echo ($search_params['relationship_goal'] ?? '') === 'dating' ? 'selected' : ''; >Dating</option>
                        <option value="friendship"  echo ($search_params['relationship_goal'] ?? '') === 'friendship' ? 'selected' : ''; >Friendship</option>
                        <option value="marriage"  echo ($search_params['relationship_goal'] ?? '') === 'marriage' ? 'selected' : ''; >Marriage</option>
                        <option value="casual"  echo ($search_params['relationship_goal'] ?? '') === 'casual' ? 'selected' : ''; >Casual</option>
                    </select>
                </div>

                <div style="margin-bottom: 1rem;">
                    <label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Keywords</label>
                    <input type="text" name="keywords" 
                           value=" echo $search_params['keywords'] ?? ''; "
                           placeholder="hiking, cooking, travel..."
                           style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;">
                </div>
            </div>

            <!-- Search Options -->
            <div style="margin-bottom: 1.5rem;">
                <h4 style="margin: 0 0 1rem 0;">Search Options</h4>

                <label style="display: flex; align-items: center; margin-bottom: 0.5rem;">
                    <input type="checkbox" name="has_photos" value="1" 
                            echo !empty($search_params['has_photos']) ? 'checked' : ''; 
                           style="margin-right: 0.5rem;">
                    Has Photos Only
                </label>

                <label style="display: flex; align-items: center; margin-bottom: 0.5rem;">
                    <input type="checkbox" name="online_recently" value="1" 
                            echo !empty($search_params['online_recently']) ? 'checked' : ''; 
                           style="margin-right: 0.5rem;">
                    Online Recently
                </label>

                <label style="display: flex; align-items: center; margin-bottom: 0.5rem;">
                    <input type="checkbox" name="new_members" value="1" 
                            echo !empty($search_params['new_members']) ? 'checked' : ''; 
                           style="margin-right: 0.5rem;">
                    New Members
                </label>

                <label style="display: flex; align-items: center; margin-bottom: 1rem;">
                    <input type="checkbox" name="exclude_liked" value="1" 
                            echo !empty($search_params['exclude_liked']) ? 'checked' : ''; 
                           style="margin-right: 0.5rem;">
                    Exclude Already Liked
                </label>

                <div style="margin-bottom: 1rem;">
                    <label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Sort By</label>
                    <select name="sort_by" style="width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;">
                        <option value="newest"  echo ($search_params['sort_by'] ?? 'newest') === 'newest' ? 'selected' : ''; >Newest First</option>
                        <option value="last_active"  echo ($search_params['sort_by'] ?? '') === 'last_active' ? 'selected' : ''; >Last Active</option>
                        <option value="compatibility"  echo ($search_params['sort_by'] ?? '') === 'compatibility' ? 'selected' : ''; >Best Match</option>
                        <option value="distance"  echo ($search_params['sort_by'] ?? '') === 'distance' ? 'selected' : ''; >Distance</option>
                    </select>
                </div>
            </div>

            <!-- Action Buttons -->
            <div style="display: flex; flex-direction: column; gap: 0.5rem;">
                <button type="submit" class="btn" style="width: 100%;">
                    🔍 Search Now
                </button>

                <a href="search.php" class="btn" style="width: 100%; background: #6c757d; text-align: center;">
                    🗑️ Clear Filters
                </a>
            </div>
        </form>

        <!-- Saved Searches -->
         if (!empty($saved_searches)): 
            <div style="border-top: 1px solid #eee; padding-top: 1.5rem; margin-top: 1.5rem;">
                <h4 style="margin: 0 0 1rem 0;">Saved Searches</h4>
                <div style="display: flex; flex-direction: column; gap: 0.5rem;">
                     foreach ($saved_searches as $saved): 
                        <a href="search.php?saved_search_id= echo $saved['id']; " 
                           style="display: block; padding: 0.5rem; background: #f8f9fa; border-radius: 5px; text-decoration: none; color: inherit;"
                           onmouseover="this.style.background='#e9ecef'"
                           onmouseout="this.style.background='#f8f9fa'">
                            <div style="font-weight: bold;"> echo $saved['search_name']; </div>
                            <small style="color: #666;">
                                Last run:  echo $saved['last_run'] ? date('M j', strtotime($saved['last_run'])) : 'Never'; 
                            </small>
                        </a>
                     endforeach; 
                </div>
            </div>
         endif; 
    </div>

    <!-- Search Results -->
    <div>
        <!-- Results Header -->
        <div style="background: white; border-radius: 10px; padding: 1.5rem; margin-bottom: 1.5rem; box-shadow: 0 5px 15px rgba(0,0,0,0.1);">
            <div style="display: flex; justify-content: between; align-items: center; margin-bottom: 1rem;">
                <h2 style="margin: 0;">
                    Search Results 
                     if ($total_count > 0): 
                        <small style="font-size: 1rem; color: #666;">( echo $total_count;  found)</small>
                     endif; 
                </h2>

                 if (!empty($results) && !isset($_GET['saved_search_id'])): 
                    <form method="POST" action="" style="display: flex; gap: 0.5rem;">
                        <input type="text" name="search_name" placeholder="Name this search" 
                               style="padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px;"
                               required>
                        <button type="submit" name="save_search" class="btn" style="background: #17a2b8;">
                            💾 Save Search
                        </button>
                         foreach ($search_params as $key => $value): 
                             if (!in_array($key, ['save_search', 'search_name'])): 
                                <input type="hidden" name=" echo $key; " value=" echo $value; ">
                             endif; 
                         endforeach; 
                    </form>
                 endif; 
            </div>

             if (!empty($search_params)): 
                <div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
                     foreach ($search_params as $key => $value): 
                         if (!empty($value) && !in_array($key, ['page', 'save_search', 'search_name'])): 
                            <span style="background: #764ba2; color: white; padding: 0.25rem 0.5rem; border-radius: 15px; font-size: 0.8rem;">
                                 echo ucfirst(str_replace('_', ' ', $key)); :  echo $value; 
                            </span>
                         endif; 
                     endforeach; 
                </div>
             endif; 
        </div>

        <!-- Results Grid -->
         if (empty($results)): 
            <div style="background: white; border-radius: 10px; padding: 3rem 2rem; text-align: center; box-shadow: 0 5px 15px rgba(0,0,0,0.1);">
                <div style="font-size: 4rem; margin-bottom: 1rem;">🔍</div>
                <h3>No Results Found</h3>
                <p style="color: #666; margin-bottom: 2rem;">
                     echo empty($search_params) ? 
                        "Use the filters to find your perfect match!" : 
                        "Try adjusting your search criteria. Nobody's perfect, but someone's close enough!"; 
                </p>
                 if (!empty($search_params)): 
                    <a href="search.php" class="btn">Clear Filters</a>
                 endif; 
            </div>
         else: 
            <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem;">
                 foreach ($results as $user): 
                    <div style="background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 5px 15px rgba(0,0,0,0.1); transition: transform 0.3s;"
                         onmouseover="this.style.transform='translateY(-5px)'"
                         onmouseout="this.style.transform='translateY(0)'">

                        <!-- User Photo -->
                        <div style="position: relative; height: 200px; background: #f8f9fa;">
                             if (!empty($user['profile_photo'])): 
                                <img src=" echo $user['profile_photo']; " 
                                     alt=" echo $user['first_name']; " 
                                     style="width: 100%; height: 100%; object-fit: cover;">
                             else: 
                                <div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #667eea, #764ba2);">
                                    <span style="font-size: 3rem; color: white;">👤</span>
                                </div>
                             endif; 

                            <!-- Already Liked Badge -->
                             if ($user['already_liked']): 
                                <div style="position: absolute; top: 0.5rem; right: 0.5rem; background: rgba(255,255,255,0.9); padding: 0.25rem 0.5rem; border-radius: 10px; font-size: 0.7rem; font-weight: bold; color: #764ba2;">
                                    ❤️ Liked
                                </div>
                             endif; 
                        </div>

                        <!-- User Info -->
                        <div style="padding: 1rem;">
                            <h4 style="margin: 0 0 0.5rem 0; font-size: 1.1rem;">
                                 echo $user['first_name']; ,  echo $user['age']; 
                                <span style="font-size: 0.9rem; color: #666;">
                                     echo $user['gender'] === 'male' ? '♂' : '♀'; 
                                </span>
                            </h4>

                             if (!empty($user['location'])): 
                                <p style="margin: 0 0 0.5rem 0; color: #666; font-size: 0.9rem;">
                                    📍  echo $user['location']; 
                                </p>
                             endif; 

                             if (!empty($user['occupation'])): 
                                <p style="margin: 0 0 0.5rem 0; color: #666; font-size: 0.9rem;">
                                    💼  echo $user['occupation']; 
                                </p>
                             endif; 

                             if (!empty($user['bio'])): 
                                <p style="margin: 0 0 1rem 0; color: #666; font-size: 0.8rem; line-height: 1.4;">
                                     echo substr($user['bio'], 0, 80) . (strlen($user['bio']) > 80 ? '...' : ''); 
                                </p>
                             endif; 

                            <div style="display: flex; gap: 0.5rem;">
                                <a href="profile.php?user_id= echo $user['id']; " 
                                   class="btn" 
                                   style="flex: 1; text-align: center; background: #17a2b8; font-size: 0.9rem;">
                                    View Profile
                                </a>
                                <a href="conversation.php?user_id= echo $user['id']; " 
                                   class="btn" 
                                   style="flex: 1; text-align: center; background: #28a745; font-size: 0.9rem;">
                                    Message
                                </a>
                            </div>
                        </div>
                    </div>
                 endforeach; 
            </div>

            <!-- Pagination -->
             if ($total_count > $limit): 
                <div style="display: flex; justify-content: center; align-items: center; gap: 1rem; margin-top: 2rem;">
                     if ($page > 1): 
                        <a href="? echo http_build_query(array_merge($search_params, ['page' => $page - 1])); " 
                           class="btn" style="background: #6c757d;">
                            ← Previous
                        </a>
                     endif; 

                    <span style="color: #666;">
                        Page  echo $page;  of  echo ceil($total_count / $limit); 
                    </span>

                     if ($has_more): 
                        <a href="? echo http_build_query(array_merge($search_params, ['page' => $page + 1])); " 
                           class="btn" style="background: #6c757d;">
                            Next →
                        </a>
                     endif; 
                </div>
             endif; 
         endif; 
    </div>
</div>

<script>
// Auto-submit form when certain filters change
document.querySelectorAll('select[name="sort_by"]').forEach(select => {
    select.addEventListener('change', function() {
        document.getElementById('search-form').submit();
    });
});

// Show/hide advanced filters
document.addEventListener('DOMContentLoaded', function() {
    const advancedToggle = document.createElement('button');
    advancedToggle.type = 'button';
    advancedToggle.innerHTML = '🔧 Show Advanced Filters';
    advancedToggle.style.cssText = 'width: 100%; padding: 0.5rem; background: #f8f9fa; border: 1px solid #ddd; border-radius: 5px; margin-bottom: 1rem; cursor: pointer;';

    const advancedSection = document.querySelector('div:has(> h4:contains("Advanced Filters"))');
    if (advancedSection) {
        advancedSection.parentNode.insertBefore(advancedToggle, advancedSection);
        advancedSection.style.display = 'none';

        advancedToggle.addEventListener('click', function() {
            if (advancedSection.style.display === 'none') {
                advancedSection.style.display = 'block';
                advancedToggle.innerHTML = '🔧 Hide Advanced Filters';
            } else {
                advancedSection.style.display = 'none';
                advancedToggle.innerHTML = '🔧 Show Advanced Filters';
            }
        });
    }
});
</script>

 include_once 'includes/footer.php'; 

4. Update Navigation

Update includes/header.php to include search link:

<!-- In the navigation section -->
<div class="nav-links">
     if(isset($_SESSION['user_id'])): 
        <a href="discover.php">Discover</a>
        <a href="search.php">Search</a> <!-- Add this line -->
        <a href="matches.php">Matches</a>
        <a href="messages.php" style="position: relative;">
            Messages

            $unread_count = get_unread_message_count($_SESSION['user_id'], $conn);
            if ($unread_count > 0): 
                <span style="position: absolute; top: -8px; right: -8px; background: #dc3545; color: white; border-radius: 50%; width: 18px; height: 18px; display: flex; align-items: center; justify-content: center; font-size: 0.7rem;">
                     echo $unread_count; 
                </span>
             endif; 
        </a>
        <a href="dashboard.php">Dashboard</a>
        <a href="logout.php" class="btn">Logout</a>
     else: 
        <a href="login.php">Login</a>
        <a href="register.php" class="btn">Find Your Match</a>
     endif; 
</div>

What We've Accomplished in Part 6

Boom! We've built a sophisticated search system:

  1. Advanced Search Filters - Age, location, height, relationship goals, and more
  2. Smart Search Algorithm - Finds matches based on multiple criteria
  3. Saved Searches - Remember your favorite search combinations
  4. Search Suggestions - Quick filters based on your preferences
  5. Pagination - Handle large result sets gracefully
  6. Real-time Results - Instant filtering and sorting

Testing Your Search System

  1. Test basic searches - Filter by age, gender, location
  2. Try advanced filters - Height, relationship goals, keywords
  3. Save searches - Create and reuse search combinations
  4. Test pagination - Navigate through large result sets
  5. Try quick searches - Use the suggested search templates

What's Still Coming

Current Status: Your dating site now has powerful search capabilities! It's like giving users a metal detector on the beach instead of making them dig with their hands.


Pro Tip: In production, you'd want to add Elasticsearch or another search engine for better performance. But for now, our MySQL queries work surprisingly well. Remember: the most important search feature is the "undo" button for when you accidentally search for people who "enjoy long walks on the beach" and realize that's everyone.

Building a Dating Website in PHP - Part 7: Real-time Magic & Notifications

Welcome back, you notification ninja! In Part 6, we built an awesome search system. Now it's time to make everything feel alive with real-time notifications. Because waiting for a page refresh to see if someone liked you is about as modern as using a carrier pigeon.

What We're Building in Part 7

1. Database Updates - Notification Central

First, let's enhance our database for real-time features. Run these SQL commands:

-- Enhanced notifications table
CREATE TABLE notifications (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    type ENUM('message', 'like', 'match', 'profile_view', 'super_like', 'system') NOT NULL,
    title VARCHAR(255) NOT NULL,
    message TEXT NOT NULL,
    related_id INT NULL COMMENT 'ID of related entity (message_id, user_id, etc)',
    is_read TINYINT(1) DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_user_id (user_id),
    INDEX idx_unread (user_id, is_read),
    INDEX idx_created (created_at)
);

-- User online status table
CREATE TABLE user_online_status (
    user_id INT PRIMARY KEY,
    is_online TINYINT(1) DEFAULT 0,
    last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    socket_id VARCHAR(100) NULL,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_online (is_online),
    INDEX idx_last_seen (last_seen)
);

-- User notification preferences
CREATE TABLE user_notification_prefs (
    user_id INT PRIMARY KEY,
    email_messages TINYINT(1) DEFAULT 1,
    email_likes TINYINT(1) DEFAULT 1,
    email_matches TINYINT(1) DEFAULT 1,
    push_messages TINYINT(1) DEFAULT 1,
    push_likes TINYINT(1) DEFAULT 1,
    push_matches TINYINT(1) DEFAULT 1,
    push_profile_views TINYINT(1) DEFAULT 1,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

-- Insert default notification preferences for existing users
INSERT INTO user_notification_prefs (user_id)
SELECT id FROM users;

-- Add last_seen to users table for fallback
ALTER TABLE users ADD COLUMN last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP;

-- Create indexes for better performance
ALTER TABLE notifications ADD INDEX idx_type_created (type, created_at);
ALTER TABLE user_online_status ADD INDEX idx_socket (socket_id);

2. WebSocket Server with Node.js

Create realtime/server.js - our real-time engine:

// realtime/server.js
const WebSocket = require('ws');
const mysql = require('mysql2/promise');
const http = require('http');
const url = require('url');

// Create HTTP server for WebSocket
const server = http.createServer();
const wss = new WebSocket.Server({ server });

// MySQL connection pool
const dbPool = mysql.createPool({
    host: 'localhost',
    user: 'root',
    password: '',
    database: 'dating_site',
    connectionLimit: 10
});

// Store connected clients
const clients = new Map();

// Authentication function
async function authenticateUser(token) {
    try {
        const [rows] = await dbPool.execute(
            'SELECT u.id, u.username, u.first_name FROM users u WHERE u.id = ?',
            [token]
        );
        return rows[0] || null;
    } catch (error) {
        console.error('Authentication error:', error);
        return null;
    }
}

// Update user online status
async function updateUserStatus(userId, isOnline, socketId = null) {
    try {
        if (isOnline) {
            await dbPool.execute(
                'INSERT INTO user_online_status (user_id, is_online, socket_id, last_seen) VALUES (?, 1, ?, NOW()) ON DUPLICATE KEY UPDATE is_online = 1, socket_id = ?, last_seen = NOW()',
                [userId, socketId, socketId]
            );
        } else {
            await dbPool.execute(
                'UPDATE user_online_status SET is_online = 0, socket_id = NULL WHERE user_id = ?',
                [userId]
            );
        }

        // Update users table as fallback
        await dbPool.execute(
            'UPDATE users SET last_seen = NOW() WHERE id = ?',
            [userId]
        );
    } catch (error) {
        console.error('Status update error:', error);
    }
}

// Send notification to user
async function sendNotificationToUser(userId, notification) {
    const userClients = Array.from(clients.values()).filter(
        client => client.userId === userId
    );

    userClients.forEach(client => {
        if (client.readyState === WebSocket.OPEN) {
            client.send(JSON.stringify({
                type: 'notification',
                data: notification
            }));
        }
    });
}

// Handle new connection
wss.on('connection', async (ws, request) => {
    const queryParams = url.parse(request.url, true).query;
    const userId = parseInt(queryParams.userId);

    if (!userId) {
        ws.close();
        return;
    }

    // Authenticate user
    const user = await authenticateUser(userId);
    if (!user) {
        ws.close();
        return;
    }

    // Store client connection
    const clientId = `${userId}_${Date.now()}`;
    clients.set(clientId, ws);
    ws.userId = userId;
    ws.clientId = clientId;

    console.log(`User ${user.first_name} (${userId}) connected`);

    // Update online status
    await updateUserStatus(userId, true, clientId);

    // Notify user's matches that they're online
    const [matches] = await dbPool.execute(
        `SELECT DISTINCT 
            CASE WHEN m.user1_id = ? THEN m.user2_id ELSE m.user1_id END as match_user_id
         FROM user_matches m 
         WHERE (m.user1_id = ? OR m.user2_id = ?) AND m.is_active = 1`,
        [userId, userId, userId]
    );

    matches.forEach(match => {
        sendNotificationToUser(match.match_user_id, {
            type: 'user_online',
            userId: userId,
            userName: user.first_name
        });
    });

    // Send unread notifications count
    const [unreadCount] = await dbPool.execute(
        'SELECT COUNT(*) as count FROM notifications WHERE user_id = ? AND is_read = 0',
        [userId]
    );

    ws.send(JSON.stringify({
        type: 'unread_count',
        count: unreadCount[0].count
    }));

    // Handle messages from client
    ws.on('message', async (data) => {
        try {
            const message = JSON.parse(data);

            switch (message.type) {
                case 'typing_start':
                    // Notify other user in conversation
                    const [conv] = await dbPool.execute(
                        `SELECT 
                            CASE WHEN c.user1_id = ? THEN c.user2_id ELSE c.user1_id END as other_user_id
                         FROM conversations c 
                         WHERE c.id = ?`,
                        [userId, message.conversationId]
                    );

                    if (conv.length > 0) {
                        sendNotificationToUser(conv[0].other_user_id, {
                            type: 'typing_start',
                            conversationId: message.conversationId,
                            userId: userId,
                            userName: user.first_name
                        });
                    }
                    break;

                case 'typing_stop':
                    const [conv2] = await dbPool.execute(
                        `SELECT 
                            CASE WHEN c.user1_id = ? THEN c.user2_id ELSE c.user1_id END as other_user_id
                         FROM conversations c 
                         WHERE c.id = ?`,
                        [userId, message.conversationId]
                    );

                    if (conv2.length > 0) {
                        sendNotificationToUser(conv2[0].other_user_id, {
                            type: 'typing_stop',
                            conversationId: message.conversationId,
                            userId: userId
                        });
                    }
                    break;

                case 'message_read':
                    // Update read status in database
                    await dbPool.execute(
                        'UPDATE messages SET is_read = 1, read_at = NOW() WHERE id = ?',
                        [message.messageId]
                    );
                    break;
            }
        } catch (error) {
            console.error('Message handling error:', error);
        }
    });

    // Handle disconnection
    ws.on('close', async () => {
        clients.delete(clientId);
        console.log(`User ${user.first_name} (${userId}) disconnected`);

        // Update online status after a delay (in case of reconnection)
        setTimeout(async () => {
            const userClients = Array.from(clients.values()).filter(
                client => client.userId === userId
            );

            if (userClients.length === 0) {
                await updateUserStatus(userId, false);

                // Notify matches that user went offline
                const [matches] = await dbPool.execute(
                    `SELECT DISTINCT 
                        CASE WHEN m.user1_id = ? THEN m.user2_id ELSE m.user1_id END as match_user_id
                     FROM user_matches m 
                     WHERE (m.user1_id = ? OR m.user2_id = ?) AND m.is_active = 1`,
                    [userId, userId, userId]
                );

                matches.forEach(match => {
                    sendNotificationToUser(match.match_user_id, {
                        type: 'user_offline',
                        userId: userId
                    });
                });
            }
        }, 5000);
    });

    // Handle errors
    ws.on('error', (error) => {
        console.error('WebSocket error:', error);
        clients.delete(clientId);
    });
});

// Broadcast system message to all users
async function broadcastSystemMessage(title, message) {
    const notification = {
        type: 'system',
        title: title,
        message: message,
        created_at: new Date()
    };

    // Store in database
    const [users] = await dbPool.execute('SELECT id FROM users WHERE is_active = 1');

    for (const user of users) {
        await dbPool.execute(
            'INSERT INTO notifications (user_id, type, title, message) VALUES (?, "system", ?, ?)',
            [user.id, title, message]
        );

        // Send real-time notification
        sendNotificationToUser(user.id, notification);
    }
}

// Start server
const PORT = process.env.PORT || 8080;
server.listen(PORT, () => {
    console.log(`WebSocket server running on port ${PORT}`);
});

// Cleanup inactive connections periodically
setInterval(() => {
    const now = Date.now();
    clients.forEach((ws, clientId) => {
        // Remove connections that haven't responded in 5 minutes
        if (now - ws.lastPing > 300000) {
            ws.terminate();
            clients.delete(clientId);
        }
    });
}, 60000);

module.exports = {
    sendNotificationToUser,
    broadcastSystemMessage
};

3. PHP Notification Helper

Create includes/notifications.php - our PHP notification handler:


// includes/notifications.php

// Create notification for user
function create_notification($user_id, $type, $title, $message, $related_id = null, $conn) {
    try {
        $query = "INSERT INTO notifications (user_id, type, title, message, related_id) 
                  VALUES (:user_id, :type, :title, :message, :related_id)";
        $stmt = $conn->prepare($query);
        $stmt->bindParam(':user_id', $user_id);
        $stmt->bindParam(':type', $type);
        $stmt->bindParam(':title', $title);
        $stmt->bindParam(':message', $message);
        $stmt->bindParam(':related_id', $related_id);
        $stmt->execute();

        $notification_id = $conn->lastInsertId();

        // Send real-time notification via WebSocket
        send_realtime_notification($user_id, [
            'id' => $notification_id,
            'type' => $type,
            'title' => $title,
            'message' => $message,
            'related_id' => $related_id,
            'created_at' => date('Y-m-d H:i:s')
        ]);

        return $notification_id;
    } catch (PDOException $e) {
        error_log("Notification creation error: " . $e->getMessage());
        return false;
    }
}

// Send real-time notification via WebSocket
function send_realtime_notification($user_id, $notification_data) {
    // In a real implementation, you'd send this to your Node.js WebSocket server
    // For now, we'll log it and handle it via JavaScript polling
    error_log("Real-time notification for user $user_id: " . json_encode($notification_data));

    // You could use Redis pub/sub or direct HTTP request to WebSocket server here
    // Example with HTTP request (commented out for now):
    /*
    $context = stream_context_create([
        'http' => [
            'method' => 'POST',
            'header' => 'Content-Type: application/json',
            'content' => json_encode([
                'user_id' => $user_id,
                'notification' => $notification_data
            ]),
            'timeout' => 1.0
        ]
    ]);

    @file_get_contents('http://localhost:8080/notify', false, $context);
    */
}

// Get user notifications
function get_user_notifications($user_id, $limit = 20, $unread_only = false, $conn) {
    $query = "SELECT * FROM notifications 
              WHERE user_id = :user_id";

    if ($unread_only) {
        $query .= " AND is_read = 0";
    }

    $query .= " ORDER BY created_at DESC LIMIT :limit";

    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
    $stmt->execute();

    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

// Mark notification as read
function mark_notification_read($notification_id, $user_id, $conn) {
    $query = "UPDATE notifications SET is_read = 1 
              WHERE id = :notification_id AND user_id = :user_id";
    $stmt = $conn->prepare($query);
    $stmt->bindParam(':notification_id', $notification_id);
    $stmt->bindParam(':user_id', $user_id);
    return $stmt->execute();
}

// Mark all notifications as read
function mark_all_notifications_read($user_id, $conn) {
    $query = "UPDATE notifications SET is_read = 1 
              WHERE user_id = :user_id AND is_read = 0";
    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    return $stmt->execute();
}

// Get unread notifications count
function get_unread_notifications_count($user_id, $conn) {
    $query = "SELECT COUNT(*) as count FROM notifications 
              WHERE user_id = :user_id AND is_read = 0";
    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->execute();

    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    return $result['count'] ?? 0;
}

// Create notification for new message
function notify_new_message($receiver_id, $sender_id, $message_id, $conn) {
    // Get sender info
    $sender_query = "SELECT first_name FROM users WHERE id = :sender_id";
    $sender_stmt = $conn->prepare($sender_query);
    $sender_stmt->bindParam(':sender_id', $sender_id);
    $sender_stmt->execute();
    $sender = $sender_stmt->fetch(PDO::FETCH_ASSOC);

    if ($sender) {
        return create_notification(
            $receiver_id,
            'message',
            'New Message',
            "You have a new message from {$sender['first_name']}",
            $message_id,
            $conn
        );
    }

    return false;
}

// Create notification for new like
function notify_new_like($receiver_id, $liker_id, $conn) {
    $liker_query = "SELECT first_name FROM users WHERE id = :liker_id";
    $liker_stmt = $conn->prepare($liker_query);
    $liker_stmt->bindParam(':liker_id', $liker_id);
    $liker_stmt->execute();
    $liker = $liker_stmt->fetch(PDO::FETCH_ASSOC);

    if ($liker) {
        return create_notification(
            $receiver_id,
            'like',
            'New Like',
            "{$liker['first_name']} liked your profile",
            $liker_id,
            $conn
        );
    }

    return false;
}

// Create notification for new match
function notify_new_match($user1_id, $user2_id, $conn) {
    // Get user info for both users
    $user_query = "SELECT id, first_name FROM users WHERE id IN (:user1_id, :user2_id)";
    $user_stmt = $conn->prepare($user_query);
    $user_stmt->bindParam(':user1_id', $user1_id);
    $user_stmt->bindParam(':user2_id', $user2_id);
    $user_stmt->execute();
    $users = $user_stmt->fetchAll(PDO::FETCH_ASSOC);

    $user1 = array_filter($users, function($u) use ($user1_id) { return $u['id'] == $user1_id; });
    $user2 = array_filter($users, function($u) use ($user2_id) { return $u['id'] == $user2_id; });

    $user1 = reset($user1);
    $user2 = reset($user2);

    if ($user1 && $user2) {
        // Notify user 1
        create_notification(
            $user1_id,
            'match',
            'It\'s a Match!',
            "You and {$user2['first_name']} liked each other",
            $user2_id,
            $conn
        );

        // Notify user 2
        create_notification(
            $user2_id,
            'match',
            'It\'s a Match!',
            "You and {$user1['first_name']} liked each other",
            $user1_id,
            $conn
        );

        return true;
    }

    return false;
}

// Check if user is online
function is_user_online($user_id, $conn) {
    $query = "SELECT is_online FROM user_online_status 
              WHERE user_id = :user_id AND is_online = 1";
    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->execute();

    return $stmt->rowCount() > 0;
}

// Get user last seen time
function get_user_last_seen($user_id, $conn) {
    $query = "SELECT COALESCE(
                (SELECT last_seen FROM user_online_status WHERE user_id = :user_id),
                (SELECT last_seen FROM users WHERE id = :user_id)
              ) as last_seen";
    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->execute();

    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    return $result['last_seen'] ?? null;
}

4. Update Existing Files for Notifications

Update includes/messaging_engine.php to add notification calls:

// In send_message function, after message is sent:
// Add this after message is successfully sent
notify_new_message($receiver_id, $sender_id, $message_id, $conn);

Update includes/matching_engine.php to add notification calls:

// In handle_user_like function, after match is created:
// Add this inside the match creation block
if ($is_match) {
    notify_new_match($liker_id, $liked_id, $conn);
} else {
    notify_new_like($liked_id, $liker_id, $conn);
}

5. Notification Center

Create notifications.php - where users manage their notifications:


// notifications.php
require_once 'config/database.php';
require_once 'includes/functions.php';
require_once 'includes/notifications.php';

if (!is_logged_in()) {
    redirect('login.php', "Login to view your notifications. You might be missing something!");
}

$database = new Database();
$conn = $database->getConnection();

$user_id = $_SESSION['user_id'];

// Handle actions
if ($_POST) {
    if (isset($_POST['mark_all_read'])) {
        if (mark_all_notifications_read($user_id, $conn)) {
            $_SESSION['flash_message'] = "All notifications marked as read! Out of sight, out of mind.";
        }
    } elseif (isset($_POST['clear_all'])) {
        // Soft delete by marking all as read (in real app, you might actually delete)
        if (mark_all_notifications_read($user_id, $conn)) {
            $_SESSION['flash_message'] = "All notifications cleared! Fresh start.";
        }
    } elseif (isset($_POST['mark_read'])) {
        $notification_id = (int)$_POST['notification_id'];
        if (mark_notification_read($notification_id, $user_id, $conn)) {
            $_SESSION['flash_message'] = "Notification marked as read!";
        }
    }

    redirect('notifications.php');
}

// Get notifications
$notifications = get_user_notifications($user_id, 50, false, $conn);
$unread_count = get_unread_notifications_count($user_id, $conn);

include_once 'includes/header.php';

<h1>Notifications 🔔</h1>
<p>Stay updated on all the action. Or ignore it. We're not judging.</p>

<!-- Quick Stats -->
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin: 2rem 0;">
    <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
        <div style="font-size: 1.5rem; font-weight: bold; color: #764ba2;"> echo count($notifications); </div>
        <div>Total</div>
    </div>
    <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
        <div style="font-size: 1.5rem; font-weight: bold; color: #dc3545;"> echo $unread_count; </div>
        <div>Unread</div>
    </div>
    <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
        <div style="font-size: 1.5rem; font-weight: bold; color: #764ba2;">

            $today_notifications = array_filter($notifications, function($n) {
                return date('Y-m-d') === date('Y-m-d', strtotime($n['created_at']));
            });
            echo count($today_notifications);

        </div>
        <div>Today</div>
    </div>
    <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
        <div style="font-size: 1.5rem; font-weight: bold; color: #764ba2;">

            $message_notifications = array_filter($notifications, function($n) {
                return $n['type'] === 'message';
            });
            echo count($message_notifications);

        </div>
        <div>Messages</div>
    </div>
</div>

<!-- Action Buttons -->
<div style="background: white; border-radius: 10px; padding: 1.5rem; margin-bottom: 1.5rem; box-shadow: 0 5px 15px rgba(0,0,0,0.1);">
    <div style="display: flex; gap: 1rem; justify-content: space-between; align-items: center;">
        <h3 style="margin: 0;">Your Notifications</h3>
        <div style="display: flex; gap: 0.5rem;">
            <form method="POST" style="margin: 0;">
                <button type="submit" name="mark_all_read" class="btn" style="background: #17a2b8;">
                    📨 Mark All Read
                </button>
            </form>
            <form method="POST" style="margin: 0;">
                <button type="submit" name="clear_all" class="btn" style="background: #6c757d;">
                    🗑️ Clear All
                </button>
            </form>
        </div>
    </div>
</div>

<!-- Notifications List -->
<div style="background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 5px 15px rgba(0,0,0,0.1);">
     if (empty($notifications)): 
        <div style="text-align: center; padding: 3rem 2rem; color: #666;">
            <div style="font-size: 4rem; margin-bottom: 1rem;">🔔</div>
            <h3>No Notifications Yet</h3>
            <p>When you get messages, likes, or matches, they'll appear here.</p>
            <div style="margin-top: 2rem;">
                <a href="discover.php" class="btn" style="margin-right: 1rem;">Find Matches</a>
                <a href="search.php" class="btn" style="background: #17a2b8;">Search People</a>
            </div>
        </div>
     else: 
         foreach ($notifications as $notification): 
            <div style="padding: 1rem 1.5rem; border-bottom: 1px solid #f8f9fa; 
                         echo !$notification['is_read'] ? 'background: #f8f9fa;' : ''; ">
                <div style="display: flex; align-items: flex-start; gap: 1rem;">
                    <!-- Notification Icon -->
                    <div style="flex-shrink: 0;">

                        $icons = [
                            'message' => '💌',
                            'like' => '❤️',
                            'match' => '💕',
                            'profile_view' => '👀',
                            'super_like' => '💎',
                            'system' => '🔔'
                        ];
                        $icon = $icons[$notification['type']] ?? '🔔';

                        <div style="font-size: 1.5rem;"> echo $icon; </div>
                    </div>

                    <!-- Notification Content -->
                    <div style="flex: 1;">
                        <div style="display: flex; justify-content: between; align-items: flex-start; margin-bottom: 0.5rem;">
                            <h4 style="margin: 0; font-size: 1rem;">
                                 echo $notification['title']; 
                                 if (!$notification['is_read']): 
                                    <span style="background: #dc3545; color: white; padding: 0.1rem 0.4rem; border-radius: 10px; font-size: 0.7rem; margin-left: 0.5rem;">New</span>
                                 endif; 
                            </h4>
                            <small style="color: #666; font-size: 0.8rem;">
                                 echo time_ago($notification['created_at']); 
                            </small>
                        </div>

                        <p style="margin: 0 0 0.5rem 0; color: #666; line-height: 1.4;">
                             echo $notification['message']; 
                        </p>

                        <!-- Action Buttons -->
                        <div style="display: flex; gap: 0.5rem;">
                             if (!$notification['is_read']): 
                                <form method="POST" style="margin: 0;">
                                    <input type="hidden" name="notification_id" value=" echo $notification['id']; ">
                                    <button type="submit" name="mark_read" 
                                            style="background: none; border: 1px solid #28a745; color: #28a745; padding: 0.25rem 0.5rem; border-radius: 5px; font-size: 0.8rem; cursor: pointer;">
                                        Mark Read
                                    </button>
                                </form>
                             endif; 

                             if ($notification['related_id']): 

                                $action_url = '';
                                $action_text = '';

                                switch ($notification['type']) {
                                    case 'message':
                                        $action_url = "conversation.php?user_id={$notification['related_id']}";
                                        $action_text = 'View Message';
                                        break;
                                    case 'like':
                                    case 'match':
                                        $action_url = "profile.php?user_id={$notification['related_id']}";
                                        $action_text = 'View Profile';
                                        break;
                                    case 'profile_view':
                                        $action_url = "profile.php?user_id={$notification['related_id']}";
                                        $action_text = 'View Their Profile';
                                        break;
                                }

                                if ($action_url && $action_text):

                                    <a href=" echo $action_url; " 
                                       style="background: #764ba2; color: white; padding: 0.25rem 0.5rem; border-radius: 5px; font-size: 0.8rem; text-decoration: none;">
                                         echo $action_text; 
                                    </a>
                                 endif; 
                             endif; 
                        </div>
                    </div>
                </div>
            </div>
         endforeach; 
     endif; 
</div>

<!-- Load More Button -->
 if (count($notifications) >= 50): 
    <div style="text-align: center; margin-top: 2rem;">
        <button class="btn" style="background: #6c757d;">
            📜 Load More Notifications
        </button>
    </div>
 endif; 

// Helper function for time ago
function time_ago($datetime) {
    $time = strtotime($datetime);
    $now = time();
    $diff = $now - $time;

    if ($diff < 60) return 'just now';
    if ($diff < 3600) return floor($diff / 60) . 'm ago';
    if ($diff < 86400) return floor($diff / 3600) . 'h ago';
    if ($diff < 604800) return floor($diff / 86400) . 'd ago';
    return date('M j, Y', $time);
}

 include_once 'includes/footer.php'; 

6. Real-time JavaScript Client

Create assets/js/realtime.js - our frontend real-time handler:

// assets/js/realtime.js
class RealtimeClient {
    constructor(userId) {
        this.userId = userId;
        this.ws = null;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 5;
        this.reconnectDelay = 1000;
        this.isConnected = false;

        this.init();
    }

    init() {
        this.connect();
        this.setupPollingFallback();
    }

    connect() {
        try {
            this.ws = new WebSocket(`ws://localhost:8080?userId=${this.userId}`);

            this.ws.onopen = () => {
                console.log('WebSocket connected');
                this.isConnected = true;
                this.reconnectAttempts = 0;
                this.onConnect();
            };

            this.ws.onmessage = (event) => {
                this.handleMessage(JSON.parse(event.data));
            };

            this.ws.onclose = () => {
                console.log('WebSocket disconnected');
                this.isConnected = false;
                this.onDisconnect();
                this.attemptReconnect();
            };

            this.ws.onerror = (error) => {
                console.error('WebSocket error:', error);
                this.isConnected = false;
            };

        } catch (error) {
            console.error('WebSocket connection failed:', error);
            this.isConnected = false;
        }
    }

    attemptReconnect() {
        if (this.reconnectAttempts < this.maxReconnectAttempts) {
            this.reconnectAttempts++;
            console.log(`Attempting to reconnect... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);

            setTimeout(() => {
                this.connect();
            }, this.reconnectDelay * this.reconnectAttempts);
        } else {
            console.log('Max reconnection attempts reached. Using polling fallback.');
        }
    }

    setupPollingFallback() {
        // Poll for new notifications every 30 seconds if WebSocket fails
        setInterval(() => {
            if (!this.isConnected) {
                this.pollNotifications();
            }
        }, 30000);
    }

    pollNotifications() {
        fetch(`/api/notifications/unread_count.php?user_id=${this.userId}`)
            .then(response => response.json())
            .then(data => {
                if (data.success && data.count > 0) {
                    this.updateNotificationBadge(data.count);
                    this.showNotificationToast('You have new notifications!');
                }
            })
            .catch(error => console.error('Polling error:', error));
    }

    handleMessage(message) {
        switch (message.type) {
            case 'notification':
                this.showNotification(message.data);
                this.updateNotificationBadge(1); // Increment count
                break;

            case 'unread_count':
                this.updateNotificationBadge(message.count);
                break;

            case 'user_online':
                this.updateUserStatus(message.userId, true, message.userName);
                break;

            case 'user_offline':
                this.updateUserStatus(message.userId, false);
                break;

            case 'typing_start':
                this.showTypingIndicator(message.conversationId, message.userId, message.userName);
                break;

            case 'typing_stop':
                this.hideTypingIndicator(message.conversationId, message.userId);
                break;
        }
    }

    showNotification(notification) {
        // Create toast notification
        const toast = this.createNotificationToast(notification);
        document.body.appendChild(toast);

        // Auto-remove after 5 seconds
        setTimeout(() => {
            if (toast.parentNode) {
                toast.parentNode.removeChild(toast);
            }
        }, 5000);

        // Play notification sound
        this.playNotificationSound();
    }

    createNotificationToast(notification) {
        const icons = {
            'message': '💌',
            'like': '❤️',
            'match': '💕',
            'profile_view': '👀',
            'super_like': '💎',
            'system': '🔔'
        };

        const toast = document.createElement('div');
        toast.className = 'notification-toast';
        toast.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background: white;
            border-left: 4px solid #764ba2;
            padding: 1rem;
            border-radius: 8px;
            box-shadow: 0 5px 15px rgba(0,0,0,0.2);
            z-index: 10000;
            max-width: 300px;
            animation: slideIn 0.3s ease-out;
        `;

        toast.innerHTML = `
            <div style="display: flex; align-items: flex-start; gap: 0.5rem;">
                <div style="font-size: 1.2rem;">${icons[notification.type] || '🔔'}</div>
                <div style="flex: 1;">
                    <div style="font-weight: bold; margin-bottom: 0.25rem;">${notification.title}</div>
                    <div style="color: #666; font-size: 0.9rem;">${notification.message}</div>
                </div>
                <button onclick="this.parentNode.parentNode.remove()" style="background: none; border: none; font-size: 1.2rem; cursor: pointer; color: #666;">×</button>
            </div>
        `;

        // Make toast clickable if it has an action
        if (notification.related_id) {
            toast.style.cursor = 'pointer';
            toast.onclick = () => {
                this.handleNotificationClick(notification);
                toast.remove();
            };
        }

        return toast;
    }

    handleNotificationClick(notification) {
        switch (notification.type) {
            case 'message':
                window.location.href = `conversation.php?user_id=${notification.related_id}`;
                break;
            case 'like':
            case 'match':
            case 'profile_view':
                window.location.href = `profile.php?user_id=${notification.related_id}`;
                break;
        }
    }

    updateNotificationBadge(count) {
        const badge = document.querySelector('.notification-badge');
        if (badge) {
            badge.textContent = count;
            badge.style.display = count > 0 ? 'flex' : 'none';
        }

        // Update browser tab title
        if (count > 0) {
            document.title = `(${count}) MateFinder`;
        } else {
            document.title = 'MateFinder';
        }
    }

    updateUserStatus(userId, isOnline, userName = '') {
        // Update user status in conversation or profile pages
        const statusElement = document.querySelector(`[data-user-id="${userId}"] .user-status`);
        if (statusElement) {
            statusElement.textContent = isOnline ? 'Online' : 'Offline';
            statusElement.style.color = isOnline ? '#28a745' : '#666';
        }
    }

    showTypingIndicator(conversationId, userId, userName) {
        const indicator = document.createElement('div');
        indicator.id = `typing-${userId}`;
        indicator.className = 'typing-indicator';
        indicator.innerHTML = `
            <div style="display: flex; align-items: center; gap: 0.5rem; color: #666; font-size: 0.9rem; margin: 0.5rem 0;">
                <div class="typing-dots">
                    <span></span>
                    <span></span>
                    <span></span>
                </div>
                ${userName} is typing...
            </div>
        `;

        const messagesContainer = document.getElementById('messages-container');
        if (messagesContainer) {
            messagesContainer.appendChild(indicator);
            messagesContainer.scrollTop = messagesContainer.scrollHeight;
        }
    }

    hideTypingIndicator(conversationId, userId) {
        const indicator = document.getElementById(`typing-${userId}`);
        if (indicator) {
            indicator.remove();
        }
    }

    sendTypingStart(conversationId) {
        if (this.isConnected) {
            this.ws.send(JSON.stringify({
                type: 'typing_start',
                conversationId: conversationId
            }));
        }
    }

    sendTypingStop(conversationId) {
        if (this.isConnected) {
            this.ws.send(JSON.stringify({
                type: 'typing_stop',
                conversationId: conversationId
            }));
        }
    }

    sendMessageRead(messageId) {
        if (this.isConnected) {
            this.ws.send(JSON.stringify({
                type: 'message_read',
                messageId: messageId
            }));
        }
    }

    playNotificationSound() {
        // Create a simple notification sound
        const audioContext = new (window.AudioContext || window.webkitAudioContext)();
        const oscillator = audioContext.createOscillator();
        const gainNode = audioContext.createGain();

        oscillator.connect(gainNode);
        gainNode.connect(audioContext.destination);

        oscillator.frequency.value = 800;
        oscillator.type = 'sine';

        gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
        gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);

        oscillator.start(audioContext.currentTime);
        oscillator.stop(audioContext.currentTime + 0.5);
    }

    onConnect() {
        console.log('Real-time features activated!');
    }

    onDisconnect() {
        console.log('Real-time features unavailable. Using fallback.');
    }
}

// Initialize real-time client when page loads
document.addEventListener('DOMContentLoaded', function() {
    const userId = document.body.getAttribute('data-user-id');
    if (userId) {
        window.realtimeClient = new RealtimeClient(userId);
    }
});

// Add CSS for animations
const style = document.createElement('style');
style.textContent = `
    @keyframes slideIn {
        from {
            transform: translateX(100%);
            opacity: 0;
        }
        to {
            transform: translateX(0);
            opacity: 1;
        }
    }

    .typing-dots {
        display: inline-flex;
        gap: 2px;
    }

    .typing-dots span {
        width: 4px;
        height: 4px;
        border-radius: 50%;
        background: #666;
        animation: typing 1.4s infinite ease-in-out;
    }

    .typing-dots span:nth-child(1) { animation-delay: -0.32s; }
    .typing-dots span:nth-child(2) { animation-delay: -0.16s; }

    @keyframes typing {
        0%, 80%, 100% {
            transform: scale(0.8);
            opacity: 0.5;
        }
        40% {
            transform: scale(1);
            opacity: 1;
        }
    }

    .notification-badge {
        position: absolute;
        top: -8px;
        right: -8px;
        background: #dc3545;
        color: white;
        border-radius: 50%;
        width: 18px;
        height: 18px;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 0.7rem;
        font-weight: bold;
    }
`;
document.head.appendChild(style);

7. Update Header for Real-time Features

Update includes/header.php to include real-time JavaScript:

<!-- Add this before closing </head> tag -->
 if (isset($_SESSION['user_id'])): 
<script src="/assets/js/realtime.js"></script>
<script>
// Pass user ID to JavaScript
document.body.setAttribute('data-user-id', ' echo $_SESSION['user_id']; ');
</script>
 endif; 

<!-- Update navigation to include notification badge -->
<a href="notifications.php" style="position: relative;">
    Notifications

    $unread_count = get_unread_notifications_count($_SESSION['user_id'], $conn);
    if ($unread_count > 0): 
        <span class="notification-badge"> echo $unread_count; </span>
     endif; 
</a>

What We've Accomplished in Part 7

Boom! We've built a sophisticated real-time notification system:

  1. WebSocket Server - Real-time communication with Node.js
  2. Instant Notifications - Messages, likes, matches in real-time
  3. Online Status - See when users are online/offline
  4. Typing Indicators - Know when someone is typing
  5. Notification Center - Manage all your notifications
  6. Fallback System - Polling when WebSockets fail
  7. Toast Notifications - Non-intrusive desktop notifications

Testing Your Real-time System

  1. Start WebSocket server - node realtime/server.js
  2. Open multiple browser tabs - Simulate different users
  3. Send messages between users - Test real-time delivery
  4. Like profiles - Test notification triggers
  5. Check online status - Verify user presence detection
  6. Test typing indicators - See real-time typing feedback

What's Still Coming

Current Status: Your dating site now feels alive with real-time features! It's like upgrading from sending letters to instant messaging - suddenly everything happens in the moment.


Pro Tip: In production, you'd want to use a proper WebSocket service like Socket.io and Redis for scaling. But for now, our custom solution works great. Remember: the most important real-time feature is letting users know when they've been left on read. We've got that covered!

Building a Dating Website in PHP - Part 8: Premium Features & Making That Money!

Welcome back, you entrepreneurial genius! In Part 7, we built an awesome real-time system. Now it's time to make this dating site actually profitable. Because "exposure" doesn't pay the server bills, and your users' love might be free, but premium features shouldn't be!

What We're Building in Part 8

1. Database Updates - Money Town!

First, let's build our financial infrastructure. Run these SQL commands:

-- Subscription plans table
CREATE TABLE subscription_plans (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    price_monthly DECIMAL(10,2) NOT NULL,
    price_yearly DECIMAL(10,2) NULL,
    features JSON NOT NULL,
    is_active TINYINT(1) DEFAULT 1,
    stripe_price_id_monthly VARCHAR(100) NULL,
    stripe_price_id_yearly VARCHAR(100) NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_active (is_active)
);

-- User subscriptions table
CREATE TABLE user_subscriptions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    plan_id INT NOT NULL,
    stripe_subscription_id VARCHAR(100) NULL,
    stripe_customer_id VARCHAR(100) NULL,
    status ENUM('active', 'canceled', 'past_due', 'unpaid', 'incomplete') DEFAULT 'active',
    current_period_start TIMESTAMP NULL,
    current_period_end TIMESTAMP NULL,
    cancel_at_period_end TINYINT(1) DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (plan_id) REFERENCES subscription_plans(id),
    UNIQUE KEY unique_user_active (user_id, status),
    INDEX idx_status (status),
    INDEX idx_period_end (current_period_end)
);

-- Payments table for tracking transactions
CREATE TABLE payments (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    subscription_id INT NULL,
    stripe_payment_intent_id VARCHAR(100) NULL,
    amount DECIMAL(10,2) NOT NULL,
    currency VARCHAR(3) DEFAULT 'USD',
    status ENUM('succeeded', 'pending', 'failed', 'canceled') DEFAULT 'pending',
    description VARCHAR(255) NULL,
    metadata JSON NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (subscription_id) REFERENCES user_subscriptions(id),
    INDEX idx_status (status),
    INDEX idx_created (created_at)
);

-- Premium features usage tracking
CREATE TABLE premium_feature_usage (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    feature_name VARCHAR(100) NOT NULL,
    usage_count INT DEFAULT 0,
    last_used TIMESTAMP NULL,
    reset_date DATE NULL,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    UNIQUE KEY unique_user_feature (user_id, feature_name),
    INDEX idx_feature (feature_name)
);

-- Insert default subscription plans
INSERT INTO subscription_plans (name, description, price_monthly, price_yearly, features) VALUES
('Free', 'Basic features to get you started', 0.00, 0.00, '["basic_matching", "limited_messages", "profile_creation"]'),
('Premium', 'Unlock all features and get more matches', 19.99, 199.99, '["unlimited_likes", "unlimited_messages", "see_who_liked_you", "advanced_search", "read_receipts", "boost_profile", "incognito_mode"]'),
('VIP', 'Maximum visibility and premium support', 49.99, 499.99, '["all_premium_features", "profile_highlight", "priority_support", "monthly_boost", "see_visitors", "advanced_analytics"]');

-- Update stripe price IDs (you'll get these from Stripe dashboard)
UPDATE subscription_plans SET 
stripe_price_id_monthly = 'price_premium_monthly',
stripe_price_id_yearly = 'price_premium_yearly'
WHERE name = 'Premium';

UPDATE subscription_plans SET 
stripe_price_id_monthly = 'price_vip_monthly',
stripe_price_id_yearly = 'price_vip_yearly'
WHERE name = 'VIP';

-- Add premium features tracking to users table
ALTER TABLE users ADD COLUMN (
    is_premium TINYINT(1) DEFAULT 0,
    premium_expires_at TIMESTAMP NULL,
    boost_available TINYINT(1) DEFAULT 0,
    incognito_mode TINYINT(1) DEFAULT 0
);

-- Create boosts table for profile boosting
CREATE TABLE profile_boosts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    boost_type ENUM('standard', 'premium', 'vip') DEFAULT 'standard',
    start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    end_time TIMESTAMP NULL,
    multiplier DECIMAL(3,2) DEFAULT 1.5,
    is_active TINYINT(1) DEFAULT 1,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_active (is_active),
    INDEX idx_end_time (end_time)
);

2. Payment Processing with Stripe

Create includes/stripe_handler.php - our money magnet:


// includes/stripe_handler.php
require_once 'vendor/autoload.php'; // You'll need to install Stripe PHP SDK

class StripeHandler {
    private $stripe;
    private $webhook_secret;

    public function __construct() {
        $this->stripe = new \Stripe\StripeClient([
            'api_key' => 'sk_test_your_stripe_secret_key', // Use env variables in production!
            'stripe_version' => '2023-10-16'
        ]);
        $this->webhook_secret = 'whsec_your_webhook_secret';
    }

    // Create a Stripe customer
    public function createCustomer($user_id, $email, $name) {
        try {
            $customer = $this->stripe->customers->create([
                'email' => $email,
                'name' => $name,
                'metadata' => [
                    'user_id' => $user_id
                ]
            ]);

            return [
                'success' => true,
                'customer_id' => $customer->id
            ];
        } catch (Exception $e) {
            error_log("Stripe customer creation error: " . $e->getMessage());
            return ['success' => false, 'error' => $e->getMessage()];
        }
    }

    // Create subscription
    public function createSubscription($customer_id, $price_id, $user_id) {
        try {
            $subscription = $this->stripe->subscriptions->create([
                'customer' => $customer_id,
                'items' => [
                    ['price' => $price_id]
                ],
                'payment_behavior' => 'default_incomplete',
                'payment_settings' => ['save_default_payment_method' => 'on_subscription'],
                'expand' => ['latest_invoice.payment_intent'],
                'metadata' => [
                    'user_id' => $user_id
                ]
            ]);

            return [
                'success' => true,
                'subscription_id' => $subscription->id,
                'client_secret' => $subscription->latest_invoice->payment_intent->client_secret,
                'status' => $subscription->status
            ];
        } catch (Exception $e) {
            error_log("Stripe subscription creation error: " . $e->getMessage());
            return ['success' => false, 'error' => $e->getMessage()];
        }
    }

    // Create one-time payment
    public function createPaymentIntent($amount, $currency, $customer_id, $metadata = []) {
        try {
            $paymentIntent = $this->stripe->paymentIntents->create([
                'amount' => $amount * 100, // Convert to cents
                'currency' => $currency,
                'customer' => $customer_id,
                'automatic_payment_methods' => [
                    'enabled' => true,
                ],
                'metadata' => $metadata
            ]);

            return [
                'success' => true,
                'client_secret' => $paymentIntent->client_secret,
                'payment_intent_id' => $paymentIntent->id
            ];
        } catch (Exception $e) {
            error_log("Stripe payment intent creation error: " . $e->getMessage());
            return ['success' => false, 'error' => $e->getMessage()];
        }
    }

    // Handle webhook events
    public function handleWebhook() {
        $payload = @file_get_contents('php://input');
        $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];

        try {
            $event = \Stripe\Webhook::constructEvent(
                $payload, $sig_header, $this->webhook_secret
            );
        } catch(\UnexpectedValueException $e) {
            http_response_code(400);
            exit();
        } catch(\Stripe\Exception\SignatureVerificationException $e) {
            http_response_code(400);
            exit();
        }

        // Handle the event
        switch ($event->type) {
            case 'customer.subscription.created':
            case 'customer.subscription.updated':
                $subscription = $event->data->object;
                $this->handleSubscriptionUpdate($subscription);
                break;

            case 'customer.subscription.deleted':
                $subscription = $event->data->object;
                $this->handleSubscriptionCancel($subscription);
                break;

            case 'invoice.payment_succeeded':
                $invoice = $event->data->object;
                $this->handlePaymentSuccess($invoice);
                break;

            case 'invoice.payment_failed':
                $invoice = $event->data->object;
                $this->handlePaymentFailure($invoice);
                break;
        }

        http_response_code(200);
    }

    private function handleSubscriptionUpdate($subscription) {
        // Update user subscription in database
        global $conn; // You'd want to pass database connection properly

        $query = "UPDATE user_subscriptions 
                 SET status = :status, 
                     current_period_start = FROM_UNIXTIME(:period_start),
                     current_period_end = FROM_UNIXTIME(:period_end),
                     updated_at = NOW()
                 WHERE stripe_subscription_id = :subscription_id";

        $stmt = $conn->prepare($query);
        $stmt->execute([
            ':status' => $subscription->status,
            ':period_start' => $subscription->current_period_start,
            ':period_end' => $subscription->current_period_end,
            ':subscription_id' => $subscription->id
        ]);

        // Update user premium status
        if ($subscription->status === 'active') {
            $this->updateUserPremiumStatus($subscription->metadata->user_id, true);
        }
    }

    private function handleSubscriptionCancel($subscription) {
        global $conn;

        $query = "UPDATE user_subscriptions 
                 SET status = 'canceled', updated_at = NOW()
                 WHERE stripe_subscription_id = :subscription_id";

        $stmt = $conn->prepare($query);
        $stmt->execute([':subscription_id' => $subscription->id]);

        // Remove premium features at period end
        if ($subscription->cancel_at_period_end) {
            $this->schedulePremiumRemoval($subscription->metadata->user_id, $subscription->current_period_end);
        } else {
            $this->updateUserPremiumStatus($subscription->metadata->user_id, false);
        }
    }

    private function updateUserPremiumStatus($user_id, $is_premium) {
        global $conn;

        $query = "UPDATE users 
                 SET is_premium = :is_premium,
                     premium_expires_at = NULL
                 WHERE id = :user_id";

        $stmt = $conn->prepare($query);
        $stmt->execute([
            ':is_premium' => $is_premium ? 1 : 0,
            ':user_id' => $user_id
        ]);
    }

    private function schedulePremiumRemoval($user_id, $timestamp) {
        global $conn;

        $query = "UPDATE users 
                 SET premium_expires_at = FROM_UNIXTIME(:timestamp)
                 WHERE id = :user_id";

        $stmt = $conn->prepare($query);
        $stmt->execute([
            ':timestamp' => $timestamp,
            ':user_id' => $user_id
        ]);
    }
}

3. Premium Features Helper

Create includes/premium_features.php - the VIP treatment:


// includes/premium_features.php

// Check if user has premium feature access
function has_premium_feature($user_id, $feature, $conn) {
    // First check if user has active premium subscription
    $premium_check = "SELECT us.status, u.is_premium 
                     FROM user_subscriptions us
                     JOIN users u ON us.user_id = u.id
                     WHERE us.user_id = :user_id 
                     AND us.status = 'active'
                     AND (us.current_period_end IS NULL OR us.current_period_end > NOW())";

    $stmt = $conn->prepare($premium_check);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->execute();

    $has_premium = $stmt->rowCount() > 0;

    if (!$has_premium) {
        return false;
    }

    // Check specific feature access based on plan
    $plan_query = "SELECT p.features 
                  FROM user_subscriptions us
                  JOIN subscription_plans p ON us.plan_id = p.id
                  WHERE us.user_id = :user_id AND us.status = 'active'";

    $plan_stmt = $conn->prepare($plan_query);
    $plan_stmt->bindParam(':user_id', $user_id);
    $plan_stmt->execute();

    if ($plan_stmt->rowCount() > 0) {
        $plan = $plan_stmt->fetch(PDO::FETCH_ASSOC);
        $features = json_decode($plan['features'], true);
        return in_array($feature, $features);
    }

    return false;
}

// Track premium feature usage
function track_premium_usage($user_id, $feature, $conn) {
    $query = "INSERT INTO premium_feature_usage (user_id, feature_name, usage_count, last_used) 
              VALUES (:user_id, :feature, 1, NOW()) 
              ON DUPLICATE KEY UPDATE 
              usage_count = usage_count + 1, 
              last_used = NOW()";

    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->bindParam(':feature', $feature);
    return $stmt->execute();
}

// Get users who liked current user (premium feature)
function get_users_who_liked_me($user_id, $conn) {
    if (!has_premium_feature($user_id, 'see_who_liked_you', $conn)) {
        return ['success' => false, 'error' => 'Premium feature required'];
    }

    $query = "SELECT u.*, ul.like_date,
              TIMESTAMPDIFF(YEAR, u.date_of_birth, CURDATE()) as age
              FROM user_likes ul
              JOIN users u ON ul.liker_id = u.id
              WHERE ul.liked_id = :user_id
              AND u.is_active = 1
              ORDER BY ul.like_date DESC";

    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->execute();

    track_premium_usage($user_id, 'see_who_liked_you', $conn);

    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

// Boost user profile (premium feature)
function boost_profile($user_id, $boost_type = 'standard', $conn) {
    if (!has_premium_feature($user_id, 'boost_profile', $conn)) {
        return ['success' => false, 'error' => 'Premium feature required'];
    }

    // Check if user has available boosts
    $user_query = "SELECT boost_available FROM users WHERE id = :user_id";
    $user_stmt = $conn->prepare($user_query);
    $user_stmt->bindParam(':user_id', $user_id);
    $user_stmt->execute();
    $user = $user_stmt->fetch(PDO::FETCH_ASSOC);

    if ($user['boost_available'] <= 0) {
        return ['success' => false, 'error' => 'No boosts available'];
    }

    // Calculate boost duration based on type
    $durations = [
        'standard' => 30, // 30 minutes
        'premium' => 60,  // 1 hour
        'vip' => 120      // 2 hours
    ];

    $duration = $durations[$boost_type] ?? 30;
    $end_time = date('Y-m-d H:i:s', strtotime("+{$duration} minutes"));

    // Create boost
    $boost_query = "INSERT INTO profile_boosts (user_id, boost_type, end_time) 
                   VALUES (:user_id, :boost_type, :end_time)";
    $boost_stmt = $conn->prepare($boost_query);
    $boost_stmt->bindParam(':user_id', $user_id);
    $boost_stmt->bindParam(':boost_type', $boost_type);
    $boost_stmt->bindParam(':end_time', $end_time);
    $boost_stmt->execute();

    // Decrement available boosts
    $update_query = "UPDATE users SET boost_available = boost_available - 1 WHERE id = :user_id";
    $update_stmt = $conn->prepare($update_query);
    $update_stmt->bindParam(':user_id', $user_id);
    $update_stmt->execute();

    track_premium_usage($user_id, 'boost_profile', $conn);

    return [
        'success' => true,
        'boost_id' => $conn->lastInsertId(),
        'end_time' => $end_time
    ];
}

// Get active boost for user
function get_active_boost($user_id, $conn) {
    $query = "SELECT * FROM profile_boosts 
              WHERE user_id = :user_id 
              AND is_active = 1 
              AND end_time > NOW()
              ORDER BY end_time DESC 
              LIMIT 1";

    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->execute();

    return $stmt->fetch(PDO::FETCH_ASSOC);
}

// Enable incognito mode (premium feature)
function enable_incognito_mode($user_id, $enable = true, $conn) {
    if (!has_premium_feature($user_id, 'incognito_mode', $conn)) {
        return ['success' => false, 'error' => 'Premium feature required'];
    }

    $query = "UPDATE users SET incognito_mode = :mode WHERE id = :user_id";
    $stmt = $conn->prepare($query);
    $stmt->bindParam(':mode', $enable, PDO::PARAM_BOOL);
    $stmt->bindParam(':user_id', $user_id);
    $result = $stmt->execute();

    if ($result && $enable) {
        track_premium_usage($user_id, 'incognito_mode', $conn);
    }

    return ['success' => $result];
}

// Get premium analytics (VIP feature)
function get_premium_analytics($user_id, $conn) {
    if (!has_premium_feature($user_id, 'advanced_analytics', $conn)) {
        return ['success' => false, 'error' => 'VIP feature required'];
    }

    $analytics = [];

    // Profile views analytics
    $views_query = "SELECT 
                   COUNT(*) as total_views,
                   COUNT(DISTINCT viewer_id) as unique_viewers,
                   DATE(created_at) as view_date
                   FROM profile_views 
                   WHERE viewed_user_id = :user_id 
                   AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
                   GROUP BY DATE(created_at)
                   ORDER BY view_date DESC";

    $views_stmt = $conn->prepare($views_query);
    $views_stmt->bindParam(':user_id', $user_id);
    $views_stmt->execute();
    $analytics['profile_views'] = $views_stmt->fetchAll(PDO::FETCH_ASSOC);

    // Like analytics
    $likes_query = "SELECT 
                   COUNT(*) as total_likes,
                   SUM(CASE WHEN super_like = 1 THEN 1 ELSE 0 END) as super_likes,
                   DATE(like_date) as like_date
                   FROM user_likes 
                   WHERE liked_id = :user_id 
                   AND like_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)
                   GROUP BY DATE(like_date)
                   ORDER BY like_date DESC";

    $likes_stmt = $conn->prepare($likes_query);
    $likes_stmt->bindParam(':user_id', $user_id);
    $likes_stmt->execute();
    $analytics['likes'] = $likes_stmt->fetchAll(PDO::FETCH_ASSOC);

    // Match conversion rate
    $conversion_query = "SELECT 
                        (SELECT COUNT(*) FROM user_matches WHERE user1_id = :user_id OR user2_id = :user_id) as matches,
                        (SELECT COUNT(*) FROM user_likes WHERE liked_id = :user_id) as likes_received";

    $conversion_stmt = $conn->prepare($conversion_query);
    $conversion_stmt->bindParam(':user_id', $user_id);
    $conversion_stmt->execute();
    $conversion = $conversion_stmt->fetch(PDO::FETCH_ASSOC);

    $analytics['conversion_rate'] = $conversion['likes_received'] > 0 ? 
        ($conversion['matches'] / $conversion['likes_received']) * 100 : 0;

    track_premium_usage($user_id, 'advanced_analytics', $conn);

    return ['success' => true, 'analytics' => $analytics];
}

// Check if user can see read receipts
function can_see_read_receipts($user_id, $conn) {
    return has_premium_feature($user_id, 'read_receipts', $conn);
}

// Get user's subscription info
function get_user_subscription($user_id, $conn) {
    $query = "SELECT us.*, p.name as plan_name, p.price_monthly, p.price_yearly, p.features
              FROM user_subscriptions us
              JOIN subscription_plans p ON us.plan_id = p.id
              WHERE us.user_id = :user_id
              ORDER BY us.created_at DESC
              LIMIT 1";

    $stmt = $conn->prepare($query);
    $stmt->bindParam(':user_id', $user_id);
    $stmt->execute();

    $subscription = $stmt->fetch(PDO::FETCH_ASSOC);

    if ($subscription) {
        $subscription['features'] = json_decode($subscription['features'], true);
        $subscription['is_active'] = $subscription['status'] === 'active' && 
                                   ($subscription['current_period_end'] === null || 
                                    strtotime($subscription['current_period_end']) > time());
    }

    return $subscription;
}

4. Premium Upgrade Page

Create premium.php - where dreams (and payments) come true:


// premium.php
require_once 'config/database.php';
require_once 'includes/functions.php';
require_once 'includes/premium_features.php';

if (!is_logged_in()) {
    redirect('login.php', "Login to upgrade your experience. Your perfect match is waiting!");
}

$database = new Database();
$conn = $database->getConnection();

$user_id = $_SESSION['user_id'];
$user_subscription = get_user_subscription($user_id, $conn);
$is_premium = $user_subscription && $user_subscription['is_active'];

// Get all active plans
$plans_query = "SELECT * FROM subscription_plans WHERE is_active = 1 ORDER BY price_monthly ASC";
$plans_stmt = $conn->prepare($plans_query);
$plans_stmt->execute();
$plans = $plans_stmt->fetchAll(PDO::FETCH_ASSOC);

// Format plans with features
foreach ($plans as &$plan) {
    $plan['features'] = json_decode($plan['features'], true);
}

// Handle subscription form
if ($_POST && isset($_POST['plan_id'])) {
    $plan_id = (int)$_POST['plan_id'];
    $billing_period = $_POST['billing_period'] ?? 'monthly';

    // In a real implementation, you'd redirect to Stripe Checkout
    // For now, we'll simulate successful payment for demo

    $_SESSION['flash_message'] = "Payment processing would happen here! Redirecting to Stripe...";
    redirect('premium_success.php?plan_id=' . $plan_id);
}

include_once 'includes/header.php';

<h1>Upgrade Your Experience 🚀</h1>
<p>Get more matches, better features, and find love faster. Because waiting is for amateurs.</p>

<!-- Current Plan Status -->
<div style="background:  echo $is_premium ? '#d4edda' : '#fff3cd'; ; 
            border: 1px solid  echo $is_premium ? '#c3e6cb' : '#ffeaa7'; ; 
            padding: 1.5rem; border-radius: 10px; margin-bottom: 2rem;">
    <div style="display: flex; justify-content: between; align-items: center;">
        <div>
            <h3 style="margin: 0 0 0.5rem 0; color:  echo $is_premium ? '#155724' : '#856404'; ;">
                 if ($is_premium): 
                    ✅ You're on  echo $user_subscription['plan_name'];  Plan
                 else: 
                    🔒 You're on Free Plan
                 endif; 
            </h3>
            <p style="margin: 0; color:  echo $is_premium ? '#155724' : '#856404'; ;">
                 if ($is_premium && $user_subscription['current_period_end']): 
                    Your plan renews on  echo date('F j, Y', strtotime($user_subscription['current_period_end'])); 
                 else: 
                    Upgrade to unlock all premium features and get more matches!
                 endif; 
            </p>
        </div>
         if ($is_premium): 
            <a href="subscription_management.php" class="btn" style="background: #6c757d;">
                Manage Subscription
            </a>
         endif; 
    </div>
</div>

<!-- Pricing Plans -->
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem; margin-bottom: 3rem;">
     foreach ($plans as $plan): 

        $is_current_plan = $is_premium && $user_subscription['plan_id'] == $plan['id'];
        $is_popular = $plan['name'] === 'Premium';

        <div style="background: white; border-radius: 15px; overflow: hidden; 
                    border: 2px solid  echo $is_popular ? '#764ba2' : '#e9ecef'; ;
                    position: relative;
                    transform:  echo $is_popular ? 'scale(1.05)' : 'scale(1)'; ;
                    box-shadow: 0 10px 30px rgba(0,0,0,0.1);">

             if ($is_popular): 
                <div style="background: #764ba2; color: white; padding: 0.5rem; text-align: center; font-weight: bold;">
                    ⭐ MOST POPULAR
                </div>
             endif; 

             if ($is_current_plan): 
                <div style="background: #28a745; color: white; padding: 0.5rem; text-align: center; font-weight: bold;">
                    ✅ CURRENT PLAN
                </div>
             endif; 

            <div style="padding: 2rem; text-align: center;">
                <h3 style="margin: 0 0 1rem 0; color: #764ba2;"> echo $plan['name']; </h3>

                <div style="margin-bottom: 1.5rem;">
                    <div style="font-size: 2.5rem; font-weight: bold; color: #333;">
                        $ echo $plan['price_monthly']; 
                    </div>
                    <div style="color: #666;">per month</div>

                     if ($plan['price_yearly'] > 0): 
                        <div style="margin-top: 0.5rem; color: #28a745; font-weight: bold;">
                            Save  echo round((1 - ($plan['price_yearly'] / ($plan['price_monthly'] * 12))) * 100); % with yearly billing
                        </div>
                        <div style="color: #666; font-size: 0.9rem;">
                            $ echo number_format($plan['price_yearly'] / 12, 2); /month
                        </div>
                     endif; 
                </div>

                <div style="text-align: left; margin-bottom: 2rem;">
                     foreach ($plan['features'] as $feature): 
                        <div style="display: flex; align-items: center; margin-bottom: 0.5rem;">
                            <span style="color: #28a745; margin-right: 0.5rem;">✓</span>
                            <span> echo ucfirst(str_replace('_', ' ', $feature)); </span>
                        </div>
                     endforeach; 
                </div>

                 if (!$is_current_plan): 
                    <form method="POST" action="">
                        <input type="hidden" name="plan_id" value=" echo $plan['id']; ">

                         if ($plan['price_yearly'] > 0): 
                            <div style="margin-bottom: 1rem;">
                                <label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
                                    Billing Period:
                                </label>
                                <div style="display: flex; gap: 0.5rem;">
                                    <label style="flex: 1;">
                                        <input type="radio" name="billing_period" value="monthly" checked 
                                               style="margin-right: 0.5rem;">
                                        Monthly: $ echo $plan['price_monthly']; 
                                    </label>
                                    <label style="flex: 1;">
                                        <input type="radio" name="billing_period" value="yearly"
                                               style="margin-right: 0.5rem;">
                                        Yearly: $ echo $plan['price_yearly']; 
                                    </label>
                                </div>
                            </div>
                         else: 
                            <input type="hidden" name="billing_period" value="monthly">
                         endif; 

                        <button type="submit" 
                                class="btn" 
                                style="width: 100%; padding: 1rem; font-size: 1.1rem;
                                       background:  echo $is_popular ? '#764ba2' : '#17a2b8'; ;">
                             echo $plan['price_monthly'] == 0 ? 'Continue Free' : 'Upgrade Now'; 
                        </button>
                    </form>
                 else: 
                    <button disabled class="btn" style="width: 100%; padding: 1rem; background: #6c757d;">
                        Current Plan
                    </button>
                 endif; 
            </div>
        </div>
     endforeach; 
</div>

<!-- Premium Features Showcase -->
<div style="background: #f8f9fa; padding: 3rem; border-radius: 15px; margin-bottom: 2rem;">
    <h2 style="text-align: center; margin-bottom: 3rem;">Why Go Premium? 🎯</h2>

    <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 2rem;">
        <div style="text-align: center;">
            <div style="font-size: 3rem; margin-bottom: 1rem;">❤️</div>
            <h3>See Who Likes You</h3>
            <p>No more guessing games. See everyone who's interested in you and match instantly.</p>
        </div>

        <div style="text-align: center;">
            <div style="font-size: 3rem; margin-bottom: 1rem;">🚀</div>
            <h3>Profile Boost</h3>
            <p>Get 10x more profile views by boosting your profile to the top of search results.</p>
        </div>

        <div style="text-align: center;">
            <div style="font-size: 3rem; margin-bottom: 1rem;">💌</div>
            <h3>Unlimited Messages</h3>
            <p>Chat with all your matches without any restrictions. Find your perfect connection.</p>
        </div>

        <div style="text-align: center;">
            <div style="font-size: 3rem; margin-bottom: 1rem;">🔍</div>
            <h3>Advanced Search</h3>
            <p>Filter by height, education, lifestyle, and more to find exactly what you're looking for.</p>
        </div>

        <div style="text-align: center;">
            <div style="font-size: 3rem; margin-bottom: 1rem;">👻</div>
            <h3>Incognito Mode</h3>
            <p>Browse profiles privately without anyone knowing you viewed them.</p>
        </div>

        <div style="text-align: center;">
            <div style="font-size: 3rem; margin-bottom: 1rem;">📊</div>
            <h3>Advanced Analytics</h3>
            <p>Get insights into your profile performance and optimize for more matches.</p>
        </div>
    </div>
</div>

<!-- Success Stories -->
<div style="background: white; padding: 2rem; border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,0.1);">
    <h3 style="text-align: center; margin-bottom: 2rem;">Success Stories from Premium Members</h3>

    <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem;">
        <div style="text-align: center;">
            <div style="font-size: 2rem; margin-bottom: 1rem;">💑</div>
            <p>"I found my husband within 2 weeks of going premium. The advanced search made all the difference!"</p>
            <strong>- Sarah, 29</strong>
        </div>

        <div style="text-align: center;">
            <div style="font-size: 2rem; margin-bottom: 1rem;">🎉</div>
            <p>"Profile boost got me 50+ matches in one day. Worth every penny!"</p>
            <strong>- Mike, 32</strong>
        </div>

        <div style="text-align: center;">
            <div style="font-size: 2rem; margin-bottom: 1rem;">🌟</div>
            <p>"Seeing who liked me saved so much time. No more swiping blindly!"</p>
            <strong>- Jessica, 27</strong>
        </div>
    </div>
</div>

<!-- FAQ Section -->
<div style="margin-top: 3rem;">
    <h3 style="text-align: center; margin-bottom: 2rem;">Frequently Asked Questions</h3>

    <div style="max-width: 800px; margin: 0 auto;">
        <div style="margin-bottom: 1rem; padding: 1rem; background: white; border-radius: 8px;">
            <strong>Q: Can I cancel my subscription anytime?</strong>
            <p style="margin: 0.5rem 0 0 0; color: #666;">A: Yes! You can cancel anytime and keep your premium features until the end of your billing period.</p>
        </div>

        <div style="margin-bottom: 1rem; padding: 1rem; background: white; border-radius: 8px;">
            <strong>Q: Is there a free trial?</strong>
            <p style="margin: 0.5rem 0 0 0; color: #666;">A: We offer a 7-day free trial for new premium members. Cancel anytime during the trial with no charges.</p>
        </div>

        <div style="margin-bottom: 1rem; padding: 1rem; background: white; border-radius: 8px;">
            <strong>Q: What payment methods do you accept?</strong>
            <p style="margin: 0.5rem 0 0 0; color: #666;">A: We accept all major credit cards, PayPal, and Apple Pay for your convenience.</p>
        </div>

        <div style="padding: 1rem; background: white; border-radius: 8px;">
            <strong>Q: How do I get a refund?</strong>
            <p style="margin: 0.5rem 0 0 0; color: #666;">A: We offer a 30-day money-back guarantee if you're not satisfied with your premium experience.</p>
        </div>
    </div>
</div>

 include_once 'includes/footer.php'; 

5. Premium Features Integration

Update discover.php to include premium features:

<!-- Add this near the like buttons in discover.php -->
 if (has_premium_feature($_SESSION['user_id'], 'see_who_liked_you', $conn)): 
    <a href="likes.php" class="btn" style="background: #ff6b6b; margin-bottom: 1rem;">
        ❤️ See Who Liked You ( echo count(get_users_who_liked_me($_SESSION['user_id'], $conn)); )
    </a>
 endif; 

 if (has_premium_feature($_SESSION['user_id'], 'boost_profile', $conn)): 
     $active_boost = get_active_boost($_SESSION['user_id'], $conn); 
     if (!$active_boost): 
        <a href="boost.php" class="btn" style="background: #ffd93d; color: #000; margin-bottom: 1rem;">
            🚀 Boost My Profile
        </a>
     else: 
        <div style="background: #d4edda; color: #155724; padding: 0.5rem; border-radius: 5px; text-align: center; margin-bottom: 1rem;">
            ✅ Profile Boost Active ( echo time_ago($active_boost['end_time']);  left)
        </div>
     endif; 
 endif; 

6. Update Header for Premium Badge

Update includes/header.php to show premium status:

<!-- In the navigation, add premium indicator -->
<div class="nav-links">
     if(isset($_SESSION['user_id'])): 
         if ($user_subscription && $user_subscription['is_active']): 
            <span style="background: #764ba2; color: white; padding: 0.25rem 0.5rem; border-radius: 10px; font-size: 0.8rem; margin-right: 1rem;">
                ⭐  echo $user_subscription['plan_name']; 
            </span>
         endif; 
        <a href="discover.php">Discover</a>
        <a href="search.php">Search</a>
        <a href="matches.php">Matches</a>
        <a href="messages.php">Messages</a>
         if (!$user_subscription || !$user_subscription['is_active']): 
            <a href="premium.php" class="btn" style="background: #ff6b6b;">
                ⭐ Go Premium
            </a>
         endif; 
        <a href="dashboard.php">Dashboard</a>
        <a href="logout.php" class="btn">Logout</a>
     else: 
        <a href="login.php">Login</a>
        <a href="register.php" class="btn">Find Your Match</a>
     endif; 
</div>

What We've Accomplished in Part 8

Ka-ching! We've built a complete premium subscription system:

  1. Subscription Management - Multiple pricing tiers with Stripe integration
  2. Payment Processing - Secure payments with webhook handling
  3. Premium Features - Exclusive features that actually provide value
  4. Feature Gating - Smart access control for premium features
  5. User Management - Subscription status tracking and management
  6. Boost System - Profile boosting for increased visibility
  7. Analytics - Premium insights and performance tracking

Testing Your Premium System

  1. Test subscription flows - Go through upgrade process
  2. Verify feature gating - Ensure premium features are properly restricted
  3. Test payment simulation - Check Stripe integration (use test mode)
  4. Verify webhook handling - Test subscription status updates
  5. Check premium UI elements - Ensure premium badges and features show correctly

What's Still Coming

Current Status: Your dating site is now a revenue-generating machine! It's like turning your hobby project into a business - suddenly those server bills don't seem so scary anymore.


Pro Tip: Always use Stripe test mode during development. Your test card number is 4242 4242 4242 4242. And remember: the best premium features are the ones that actually help users find love faster, not just the ones that look shiny.

Building a Dating Website in PHP - Part 9: Mobile Mastery & PWA Power

Welcome back, you mobile maestro! In Part 8, we built a money-making premium system. Now it's time to make sure your dating site works flawlessly on every device. Because love happens everywhere - not just when people are sitting at their desks!

What We're Building in Part 9

1. Responsive CSS Overhaul

Create assets/css/responsive.css - our mobile-first masterpiece:

/* assets/css/responsive.css */

/* Base Mobile-First Styles */
:root {
    --primary-color: #764ba2;
    --secondary-color: #667eea;
    --success-color: #28a745;
    --danger-color: #dc3545;
    --warning-color: #ffc107;
    --info-color: #17a2b8;
    --light-color: #f8f9fa;
    --dark-color: #343a40;
    --border-radius: 10px;
    --box-shadow: 0 5px 15px rgba(0,0,0,0.1);
    --transition: all 0.3s ease;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
    line-height: 1.6;
    color: #333;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
}

/* Container */
.container {
    width: 100%;
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 1rem;
}

/* Navigation - Mobile First */
.navbar {
    background: rgba(255, 255, 255, 0.95);
    backdrop-filter: blur(10px);
    padding: 1rem 0;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 1000;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.nav-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.logo {
    font-size: 1.25rem;
    font-weight: bold;
    color: var(--primary-color);
    text-decoration: none;
}

/* Mobile Menu Toggle */
.menu-toggle {
    display: none;
    flex-direction: column;
    justify-content: space-between;
    width: 24px;
    height: 18px;
    background: none;
    border: none;
    cursor: pointer;
}

.menu-toggle span {
    display: block;
    height: 2px;
    width: 100%;
    background: var(--primary-color);
    border-radius: 1px;
    transition: var(--transition);
}

.nav-links {
    display: flex;
    align-items: center;
    gap: 1rem;
}

.nav-links a {
    text-decoration: none;
    color: #333;
    font-weight: 500;
    padding: 0.5rem 0.75rem;
    border-radius: var(--border-radius);
    transition: var(--transition);
}

.nav-links a:hover {
    background: var(--light-color);
}

.nav-links .btn {
    background: var(--primary-color);
    color: white;
    padding: 0.5rem 1rem;
    border-radius: var(--border-radius);
    text-decoration: none;
}

/* Main Content */
.main-content {
    margin-top: 80px; /* Account for fixed navbar */
    padding: 1rem 0;
    min-height: calc(100vh - 80px);
}

/* Cards */
.card {
    background: white;
    border-radius: var(--border-radius);
    box-shadow: var(--box-shadow);
    overflow: hidden;
    transition: var(--transition);
}

.card:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}

/* Buttons */
.btn {
    display: inline-block;
    background: var(--primary-color);
    color: white;
    padding: 0.75rem 1.5rem;
    border: none;
    border-radius: var(--border-radius);
    text-decoration: none;
    font-weight: 500;
    cursor: pointer;
    transition: var(--transition);
    text-align: center;
}

.btn:hover {
    background: var(--secondary-color);
    transform: translateY(-1px);
}

.btn-block {
    display: block;
    width: 100%;
}

/* Forms */
.form-group {
    margin-bottom: 1rem;
}

.form-label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: 500;
}

.form-control {
    width: 100%;
    padding: 0.75rem;
    border: 1px solid #ddd;
    border-radius: var(--border-radius);
    font-size: 1rem;
    transition: var(--transition);
}

.form-control:focus {
    outline: none;
    border-color: var(--primary-color);
    box-shadow: 0 0 0 3px rgba(118, 75, 162, 0.1);
}

/* Grid System */
.grid {
    display: grid;
    gap: 1.5rem;
}

.grid-1 { grid-template-columns: 1fr; }
.grid-2 { grid-template-columns: 1fr; }
.grid-3 { grid-template-columns: 1fr; }
.grid-4 { grid-template-columns: 1fr; }

/* Swipe Cards for Mobile */
.swipe-container {
    position: relative;
    height: 70vh;
    max-height: 600px;
    margin: 0 auto;
}

.swipe-card {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    background: white;
    border-radius: var(--border-radius);
    box-shadow: var(--box-shadow);
    overflow: hidden;
    transition: transform 0.3s ease;
}

/* Touch Interactions */
.touch-area {
    -webkit-tap-highlight-color: transparent;
    user-select: none;
}

/* Loading States */
.loading {
    opacity: 0.7;
    pointer-events: none;
}

.loading::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 20px;
    height: 20px;
    margin: -10px 0 0 -10px;
    border: 2px solid #f3f3f3;
    border-top: 2px solid var(--primary-color);
    border-radius: 50%;
    animation: spin 1s linear infinite;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

/* Mobile Bottom Navigation */
.bottom-nav {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    background: white;
    display: flex;
    justify-content: space-around;
    padding: 0.75rem 0;
    box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
    z-index: 1000;
}

.bottom-nav-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-decoration: none;
    color: #666;
    font-size: 0.75rem;
    padding: 0.5rem;
    border-radius: var(--border-radius);
    transition: var(--transition);
}

.bottom-nav-item.active {
    color: var(--primary-color);
}

.bottom-nav-icon {
    font-size: 1.25rem;
    margin-bottom: 0.25rem;
}

/* Tablet Styles */
@media (min-width: 768px) {
    .container {
        padding: 0 2rem;
    }

    .grid-2 { grid-template-columns: repeat(2, 1fr); }
    .grid-3 { grid-template-columns: repeat(3, 1fr); }
    .grid-4 { grid-template-columns: repeat(4, 1fr); }

    .nav-links {
        gap: 1.5rem;
    }

    .main-content {
        margin-top: 90px;
        min-height: calc(100vh - 90px);
    }

    .bottom-nav {
        display: none;
    }
}

/* Desktop Styles */
@media (min-width: 1024px) {
    .menu-toggle {
        display: none;
    }

    .nav-links {
        display: flex !important;
    }

    .grid-2 { grid-template-columns: repeat(2, 1fr); }
    .grid-3 { grid-template-columns: repeat(3, 1fr); }
    .grid-4 { grid-template-columns: repeat(4, 1fr); }
}

/* Large Desktop */
@media (min-width: 1200px) {
    .container {
        max-width: 1200px;
    }
}

/* Mobile Menu */
@media (max-width: 767px) {
    .menu-toggle {
        display: flex;
    }

    .nav-links {
        display: none;
        position: absolute;
        top: 100%;
        left: 0;
        right: 0;
        background: white;
        flex-direction: column;
        padding: 1rem;
        box-shadow: 0 5px 15px rgba(0,0,0,0.1);
    }

    .nav-links.active {
        display: flex;
    }

    .nav-links a {
        width: 100%;
        text-align: center;
        padding: 0.75rem;
    }

    /* Hide desktop navigation elements on mobile */
    .desktop-only {
        display: none;
    }
}

/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
    body {
        background: linear-gradient(135deg, #2d3748 0%, #4a5568 100%);
    }

    .navbar {
        background: rgba(45, 55, 72, 0.95);
    }

    .nav-links a {
        color: #e2e8f0;
    }

    .card {
        background: #2d3748;
        color: #e2e8f0;
    }

    .form-control {
        background: #4a5568;
        border-color: #718096;
        color: #e2e8f0;
    }
}

/* Reduced Motion */
@media (prefers-reduced-motion: reduce) {
    * {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* High Contrast */
@media (prefers-contrast: high) {
    :root {
        --primary-color: #000000;
        --secondary-color: #333333;
    }

    .btn {
        border: 2px solid currentColor;
    }
}

2. PWA Manifest

Create manifest.json - our app identity:

{
  "name": "MateFinder - Find Your Perfect Match",
  "short_name": "MateFinder",
  "description": "The modern dating app that helps you find meaningful connections",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#764ba2",
  "theme_color": "#764ba2",
  "orientation": "portrait-primary",
  "scope": "/",
  "lang": "en-US",
  "categories": ["dating", "social", "lifestyle"],
  "icons": [
    {
      "src": "/assets/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/assets/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/assets/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/assets/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/assets/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/assets/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/assets/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/assets/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ],
  "screenshots": [
    {
      "src": "/assets/screenshots/mobile-discover.png",
      "sizes": "375x667",
      "type": "image/png",
      "form_factor": "narrow"
    },
    {
      "src": "/assets/screenshots/tablet-messages.png",
      "sizes": "768x1024",
      "type": "image/png",
      "form_factor": "wide"
    }
  ],
  "shortcuts": [
    {
      "name": "Discover Matches",
      "short_name": "Discover",
      "description": "Browse potential matches",
      "url": "/discover.php",
      "icons": [
        {
          "src": "/assets/icons/discover-96x96.png",
          "sizes": "96x96"
        }
      ]
    },
    {
      "name": "Messages",
      "short_name": "Messages",
      "description": "Check your conversations",
      "url": "/messages.php",
      "icons": [
        {
          "src": "/assets/icons/messages-96x96.png",
          "sizes": "96x96"
        }
      ]
    },
    {
      "name": "My Profile",
      "short_name": "Profile",
      "description": "View and edit your profile",
      "url": "/profile.php",
      "icons": [
        {
          "src": "/assets/icons/profile-96x96.png",
          "sizes": "96x96"
        }
      ]
    }
  ],
  "related_applications": [],
  "prefer_related_applications": false
}

3. Service Worker

Create sw.js - our offline hero:

// sw.js - Service Worker for MateFinder PWA

const CACHE_NAME = 'matefinder-v1.0.0';
const STATIC_CACHE = 'static-v1';
const DYNAMIC_CACHE = 'dynamic-v1';

// Assets to cache immediately
const STATIC_ASSETS = [
    '/',
    '/index.php',
    '/login.php',
    '/register.php',
    '/assets/css/responsive.css',
    '/assets/js/app.js',
    '/assets/js/realtime.js',
    '/assets/icons/icon-192x192.png',
    '/assets/icons/icon-512x512.png',
    '/manifest.json',
    '/offline.html'
];

// Install event - cache static assets
self.addEventListener('install', (event) => {
    console.log('Service Worker: Installing...');

    event.waitUntil(
        caches.open(STATIC_CACHE)
            .then((cache) => {
                console.log('Service Worker: Caching static assets');
                return cache.addAll(STATIC_ASSETS);
            })
            .then(() => {
                console.log('Service Worker: Install completed');
                return self.skipWaiting();
            })
            .catch((error) => {
                console.error('Service Worker: Install failed', error);
            })
    );
});

// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
    console.log('Service Worker: Activating...');

    event.waitUntil(
        caches.keys().then((cacheNames) => {
            return Promise.all(
                cacheNames.map((cache) => {
                    if (cache !== STATIC_CACHE && cache !== DYNAMIC_CACHE) {
                        console.log('Service Worker: Deleting old cache', cache);
                        return caches.delete(cache);
                    }
                })
            );
        }).then(() => {
            console.log('Service Worker: Activate completed');
            return self.clients.claim();
        })
    );
});

// Fetch event - serve from cache or network
self.addEventListener('fetch', (event) => {
    // Skip non-GET requests
    if (event.request.method !== 'GET') {
        return;
    }

    // Skip Chrome extensions
    if (event.request.url.indexOf('chrome-extension') !== -1) {
        return;
    }

    event.respondWith(
        caches.match(event.request)
            .then((response) => {
                // Return cached version or fetch from network
                return response || fetch(event.request)
                    .then((fetchResponse) => {
                        // Check if we received a valid response
                        if (!fetchResponse || fetchResponse.status !== 200 || fetchResponse.type !== 'basic') {
                            return fetchResponse;
                        }

                        // Clone the response
                        const responseToCache = fetchResponse.clone();

                        // Cache the fetched response
                        caches.open(DYNAMIC_CACHE)
                            .then((cache) => {
                                // Only cache same-origin requests
                                if (event.request.url.startsWith(self.location.origin)) {
                                    cache.put(event.request, responseToCache);
                                }
                            });

                        return fetchResponse;
                    })
                    .catch(() => {
                        // If both cache and network fail, show offline page
                        if (event.request.destination === 'document') {
                            return caches.match('/offline.html');
                        }

                        // For API requests, return a generic response
                        if (event.request.url.includes('/api/')) {
                            return new Response(
                                JSON.stringify({ 
                                    error: 'You are offline',
                                    offline: true 
                                }),
                                { 
                                    headers: { 'Content-Type': 'application/json' } 
                                }
                            );
                        }
                    });
            })
    );
});

// Background sync for offline actions
self.addEventListener('sync', (event) => {
    console.log('Service Worker: Background sync', event.tag);

    if (event.tag === 'background-sync-messages') {
        event.waitUntil(syncMessages());
    } else if (event.tag === 'background-sync-likes') {
        event.waitUntil(syncLikes());
    }
});

// Sync pending messages when back online
async function syncMessages() {
    const db = await openMessageDB();
    const pendingMessages = await getPendingMessages(db);

    for (const message of pendingMessages) {
        try {
            await fetch('/api/messages/send.php', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(message)
            });

            await markMessageAsSent(db, message.id);
        } catch (error) {
            console.error('Failed to sync message:', error);
        }
    }
}

// Sync pending likes when back online
async function syncLikes() {
    const db = await openLikeDB();
    const pendingLikes = await getPendingLikes(db);

    for (const like of pendingLikes) {
        try {
            await fetch('/api/likes/send.php', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(like)
            });

            await markLikeAsSent(db, like.id);
        } catch (error) {
            console.error('Failed to sync like:', error);
        }
    }
}

// Push notifications
self.addEventListener('push', (event) => {
    if (!event.data) return;

    const data = event.data.json();
    const options = {
        body: data.body,
        icon: '/assets/icons/icon-192x192.png',
        badge: '/assets/icons/badge-72x72.png',
        image: data.image,
        data: data.url,
        actions: [
            {
                action: 'view',
                title: 'View',
                icon: '/assets/icons/eye-24x24.png'
            },
            {
                action: 'dismiss',
                title: 'Dismiss',
                icon: '/assets/icons/x-24x24.png'
            }
        ],
        tag: data.tag || 'general',
        renotify: true,
        requireInteraction: true
    };

    event.waitUntil(
        self.registration.showNotification(data.title, options)
    );
});

// Notification click handler
self.addEventListener('notificationclick', (event) => {
    event.notification.close();

    if (event.action === 'view' && event.notification.data) {
        event.waitUntil(
            clients.matchAll({ type: 'window' }).then((windowClients) => {
                // Check if app is already open
                for (const client of windowClients) {
                    if (client.url === event.notification.data && 'focus' in client) {
                        return client.focus();
                    }
                }

                // Open new window if app isn't open
                if (clients.openWindow) {
                    return clients.openWindow(event.notification.data);
                }
            })
        );
    }
});

// IndexedDB for offline data storage
function openMessageDB() {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open('MateFinderMessages', 1);

        request.onerror = () => reject(request.error);
        request.onsuccess = () => resolve(request.result);

        request.onupgradeneeded = (event) => {
            const db = event.target.result;

            if (!db.objectStoreNames.contains('pendingMessages')) {
                const store = db.createObjectStore('pendingMessages', { 
                    keyPath: 'id',
                    autoIncrement: true 
                });
                store.createIndex('timestamp', 'timestamp');
            }
        };
    });
}

function getPendingMessages(db) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['pendingMessages'], 'readonly');
        const store = transaction.objectStore('pendingMessages');
        const request = store.getAll();

        request.onerror = () => reject(request.error);
        request.onsuccess = () => resolve(request.result);
    });
}

4. Mobile-Optimized Discover Page

Update discover.php with mobile enhancements:


// discover.php (Mobile-optimized version)
// ... existing PHP code ...

include_once 'includes/header.php';

<!-- Mobile Bottom Navigation -->
<nav class="bottom-nav mobile-only">
    <a href="discover.php" class="bottom-nav-item active">
        <span class="bottom-nav-icon">🔍</span>
        <span>Discover</span>
    </a>
    <a href="matches.php" class="bottom-nav-item">
        <span class="bottom-nav-icon">💕</span>
        <span>Matches</span>
    </a>
    <a href="messages.php" class="bottom-nav-item">
        <span class="bottom-nav-icon">💌</span>
        <span>Messages</span>
    </a>
    <a href="profile.php" class="bottom-nav-item">
        <span class="bottom-nav-icon">👤</span>
        <span>Profile</span>
    </a>
</nav>

<div class="container">
    <div class="main-content">
        <h1 style="text-align: center; margin-bottom: 1rem;">Discover 🔥</h1>
        <p style="text-align: center; margin-bottom: 2rem; color: #666;">
            Swipe right on people who catch your eye
        </p>

         if (empty($potential_matches)): 
            <div class="card" style="text-align: center; padding: 3rem 2rem;">
                <div style="font-size: 4rem; margin-bottom: 1rem;">😴</div>
                <h3>Out of Potential Matches!</h3>
                <p style="color: #666; margin-bottom: 2rem;">
                    We've shown you everyone in your area.
                </p>
                <div class="grid grid-2" style="gap: 1rem;">
                    <a href="preferences.php" class="btn">Adjust Preferences</a>
                    <a href="search.php" class="btn" style="background: var(--info-color);">Advanced Search</a>
                </div>
            </div>
         else: 
            <!-- Swipe Container for Mobile -->
            <div class="swipe-container touch-area" id="swipe-container">
                 foreach ($potential_matches as $index => $match): 
                    <div class="swipe-card card  echo $index === 0 ? 'active' : 'inactive'; " 
                         data-user-id=" echo $match['id']; "
                         style=" echo $index === 0 ? '' : 'display: none;'; ">

                        <!-- Match Photo -->
                        <div style="position: relative; height: 60vh; max-height: 500px; background: var(--light-color);">
                             if (!empty($match['profile_photo'])): 
                                <img src=" echo $match['profile_photo']; " 
                                     alt=" echo $match['first_name']; " 
                                     style="width: 100%; height: 100%; object-fit: cover;"
                                     loading="lazy">
                             else: 
                                <div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; 
                                            background: linear-gradient(135deg, var(--secondary-color), var(--primary-color));">
                                    <span style="font-size: 4rem; color: white;">👤</span>
                                </div>
                             endif; 

                            <!-- Compatibility Badge -->
                            <div style="position: absolute; top: 1rem; right: 1rem; background: rgba(255,255,255,0.9); 
                                        padding: 0.5rem 1rem; border-radius: 20px; font-weight: bold; color: var(--primary-color);">
                                 echo $match['compatibility_score']; % Match
                            </div>

                            <!-- Action Overlay -->
                            <div style="position: absolute; bottom: 0; left: 0; right: 0; 
                                        background: linear-gradient(transparent, rgba(0,0,0,0.7)); 
                                        padding: 2rem 1.5rem 1.5rem; color: white;">
                                <h2 style="margin: 0 0 0.5rem 0; font-size: 1.5rem;">
                                     echo $match['first_name']; ,  echo $match['age']; 
                                </h2>

                                 if (!empty($match['occupation'])): 
                                    <p style="margin: 0 0 0.5rem 0; opacity: 0.9;">
                                        💼  echo $match['occupation']; 
                                    </p>
                                 endif; 

                                 if (!empty($match['location'])): 
                                    <p style="margin: 0; opacity: 0.9;">
                                        📍  echo $match['location']; 
                                    </p>
                                 endif; 
                            </div>
                        </div>

                        <!-- Match Info -->
                        <div style="padding: 1.5rem;">
                             if (!empty($match['bio'])): 
                                <p style="margin-bottom: 1.5rem; line-height: 1.5;">
                                     echo nl2br(htmlspecialchars($match['bio'])); 
                                </p>
                             endif; 

                            <!-- Interests -->
                             if (!empty($match['interests'])): 
                                <div style="margin-bottom: 1.5rem;">
                                    <strong>🎨 Interests:</strong>
                                    <div style="margin-top: 0.5rem; display: flex; flex-wrap: wrap; gap: 0.5rem;">

                                        $interests = explode(',', $match['interests']);
                                        foreach (array_slice($interests, 0, 6) as $interest): 
                                            $interest = trim($interest);
                                            if (!empty($interest)):

                                            <span style="background: var(--light-color); padding: 0.25rem 0.75rem; 
                                                      border-radius: 15px; font-size: 0.8rem;">
                                                 echo htmlspecialchars($interest); 
                                            </span>

                                            endif;
                                        endforeach; 

                                    </div>
                                </div>
                             endif; 

                            <!-- Action Buttons - Mobile Optimized -->
                            <div class="grid grid-3" style="gap: 0.5rem;">
                                <!-- Pass Button -->
                                <form method="POST" action="" class="swipe-form" data-action="pass">
                                    <input type="hidden" name="action" value="pass">
                                    <input type="hidden" name="user_id" value=" echo $match['id']; ">
                                    <button type="button" class="btn swipe-btn pass-btn" 
                                            style="background: var(--danger-color); padding: 1rem;">
                                        <span style="font-size: 1.5rem;">✖️</span>
                                    </button>
                                </form>

                                <!-- Super Like Button -->
                                <form method="POST" action="" class="swipe-form" data-action="super_like">
                                    <input type="hidden" name="action" value="like">
                                    <input type="hidden" name="user_id" value=" echo $match['id']; ">
                                    <input type="hidden" name="super_like" value="1">
                                    <button type="button" class="btn swipe-btn super-like-btn" 
                                            style="background: var(--info-color); padding: 1rem;">
                                        <span style="font-size: 1.5rem;">💎</span>
                                    </button>
                                </form>

                                <!-- Like Button -->
                                <form method="POST" action="" class="swipe-form" data-action="like">
                                    <input type="hidden" name="action" value="like">
                                    <input type="hidden" name="user_id" value=" echo $match['id']; ">
                                    <button type="button" class="btn swipe-btn like-btn" 
                                            style="background: var(--success-color); padding: 1rem;">
                                        <span style="font-size: 1.5rem;">❤️</span>
                                    </button>
                                </form>
                            </div>

                            <!-- Quick Info -->
                            <div style="text-align: center; margin-top: 1rem; color: #666; font-size: 0.8rem;">
                                 echo count($potential_matches) - $index - 1;  more profiles to view
                            </div>
                        </div>
                    </div>
                 endforeach; 
            </div>

            <!-- Swipe Instructions for Mobile -->
            <div class="card mobile-only" style="text-align: center; padding: 1rem; margin-top: 1rem;">
                <div class="grid grid-3" style="gap: 1rem;">
                    <div>
                        <div style="font-size: 1.5rem;">←</div>
                        <div style="font-size: 0.8rem; color: #666;">Swipe Left to Pass</div>
                    </div>
                    <div>
                        <div style="font-size: 1.5rem;">↑</div>
                        <div style="font-size: 0.8rem; color: #666;">Swipe Up for Super Like</div>
                    </div>
                    <div>
                        <div style="font-size: 1.5rem;">→</div>
                        <div style="font-size: 0.8rem; color: #666;">Swipe Right to Like</div>
                    </div>
                </div>
            </div>
         endif; 

        <!-- Premium Features Callout -->
         if (!has_premium_feature($_SESSION['user_id'], 'unlimited_likes', $conn)): 
            <div class="card" style="background: linear-gradient(135deg, #ffd93d, #ff6b6b); color: #000; 
                                    text-align: center; padding: 1.5rem; margin-top: 2rem;">
                <h3 style="margin: 0 0 0.5rem 0;">🚀 Get Unlimited Likes!</h3>
                <p style="margin: 0 0 1rem 0; opacity: 0.9;">
                    Upgrade to Premium for unlimited likes and more matches
                </p>
                <a href="premium.php" class="btn" style="background: #000; color: #fff;">
                    Go Premium
                </a>
            </div>
         endif; 
    </div>
</div>

<!-- Mobile Swipe JavaScript -->
<script>
class SwipeManager {
    constructor() {
        this.container = document.getElementById('swipe-container');
        this.cards = Array.from(this.container.querySelectorAll('.swipe-card'));
        this.currentIndex = 0;
        this.startX = 0;
        this.currentX = 0;
        this.isDragging = false;

        this.init();
    }

    init() {
        // Touch events for mobile
        this.container.addEventListener('touchstart', this.handleTouchStart.bind(this));
        this.container.addEventListener('touchmove', this.handleTouchMove.bind(this));
        this.container.addEventListener('touchend', this.handleTouchEnd.bind(this));

        // Mouse events for desktop
        this.container.addEventListener('mousedown', this.handleMouseDown.bind(this));
        this.container.addEventListener('mousemove', this.handleMouseMove.bind(this));
        this.container.addEventListener('mouseup', this.handleMouseUp.bind(this));
        this.container.addEventListener('mouseleave', this.handleMouseUp.bind(this));

        // Button events
        this.container.querySelectorAll('.swipe-btn').forEach(btn => {
            btn.addEventListener('click', this.handleButtonClick.bind(this));
        });

        // Prevent image drag
        this.container.querySelectorAll('img').forEach(img => {
            img.addEventListener('dragstart', (e) => e.preventDefault());
        });
    }

    handleTouchStart(e) {
        this.startDrag(e.touches[0].clientX);
    }

    handleTouchMove(e) {
        this.drag(e.touches[0].clientX);
    }

    handleTouchEnd() {
        this.endDrag();
    }

    handleMouseDown(e) {
        e.preventDefault();
        this.startDrag(e.clientX);
    }

    handleMouseMove(e) {
        if (this.isDragging) {
            this.drag(e.clientX);
        }
    }

    handleMouseUp() {
        this.endDrag();
    }

    startDrag(clientX) {
        this.isDragging = true;
        this.startX = clientX;
        this.currentCard = this.cards[this.currentIndex];
        this.currentCard.style.transition = 'none';
    }

    drag(clientX) {
        if (!this.isDragging) return;

        this.currentX = clientX - this.startX;
        const rotation = this.currentX * 0.1;

        this.currentCard.style.transform = `translateX(${this.currentX}px) rotate(${rotation}deg)`;

        // Change opacity based on swipe direction
        const opacity = 1 - Math.abs(this.currentX) / 200;
        this.currentCard.style.opacity = Math.max(opacity, 0.5);
    }

    endDrag() {
        if (!this.isDragging) return;

        this.isDragging = false;
        this.currentCard.style.transition = 'transform 0.3s ease, opacity 0.3s ease';

        const threshold = 100;

        if (Math.abs(this.currentX) > threshold) {
            // Swipe action
            if (this.currentX > 0) {
                this.like();
            } else {
                this.pass();
            }
        } else {
            // Return to center
            this.resetCard();
        }
    }

    handleButtonClick(e) {
        const button = e.currentTarget;
        const form = button.closest('.swipe-form');
        const action = form.dataset.action;

        if (action === 'like') {
            this.like();
        } else if (action === 'pass') {
            this.pass();
        } else if (action === 'super_like') {
            this.superLike();
        }
    }

    like() {
        this.swipeCard('like', 1);
    }

    pass() {
        this.swipeCard('pass', -1);
    }

    superLike() {
        this.swipeCard('super_like', 1);
    }

    swipeCard(action, direction) {
        const card = this.cards[this.currentIndex];
        const form = card.querySelector(`[data-action="${action}"]`);

        // Animate card out
        card.style.transform = `translateX(${direction * 500}px) rotate(${direction * 30}deg)`;
        card.style.opacity = '0';

        // Submit form after animation
        setTimeout(() => {
            if (form) {
                // In a real app, you'd use AJAX here
                form.submit();
            }

            // Show next card
            this.showNextCard();
        }, 300);
    }

    showNextCard() {
        this.currentIndex++;

        if (this.currentIndex < this.cards.length) {
            this.cards[this.currentIndex].style.display = 'block';
        } else {
            // No more cards
            this.container.innerHTML = `
                <div class="card" style="text-align: center; padding: 3rem 2rem;">
                    <div style="font-size: 4rem; margin-bottom: 1rem;">🎉</div>
                    <h3>You've seen everyone!</h3>
                    <p style="color: #666; margin-bottom: 2rem;">
                        Check back later for new matches or adjust your preferences.
                    </p>
                    <div class="grid grid-2" style="gap: 1rem;">
                        <a href="preferences.php" class="btn">Adjust Preferences</a>
                        <a href="search.php" class="btn" style="background: var(--info-color);">Advanced Search</a>
                    </div>
                </div>
            `;
        }
    }

    resetCard() {
        this.currentCard.style.transform = 'translateX(0) rotate(0deg)';
        this.currentCard.style.opacity = '1';
    }
}

// Initialize swipe manager when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
    if (document.getElementById('swipe-container')) {
        new SwipeManager();
    }
});

// PWA Installation Prompt
let deferredPrompt;
const installButton = document.createElement('button');

window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault();
    deferredPrompt = e;

    // Show install prompt
    installButton.style.cssText = `
        position: fixed;
        bottom: 80px;
        right: 20px;
        background: var(--primary-color);
        color: white;
        border: none;
        border-radius: 50%;
        width: 60px;
        height: 60px;
        font-size: 1.5rem;
        box-shadow: var(--box-shadow);
        z-index: 1000;
        cursor: pointer;
    `;
    installButton.innerHTML = '📱';
    installButton.title = 'Install App';

    installButton.addEventListener('click', async () => {
        if (deferredPrompt) {
            deferredPrompt.prompt();
            const { outcome } = await deferredPrompt.userChoice;

            if (outcome === 'accepted') {
                installButton.style.display = 'none';
            }

            deferredPrompt = null;
        }
    });

    document.body.appendChild(installButton);
});

// Register service worker for PWA
if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js')
            .then((registration) => {
                console.log('SW registered: ', registration);
            })
            .catch((registrationError) => {
                console.log('SW registration failed: ', registrationError);
            });
    });
}

// Online/Offline detection
window.addEventListener('online', () => {
    showToast('You are back online!', 'success');
});

window.addEventListener('offline', () => {
    showToast('You are offline. Some features may not work.', 'warning');
});

function showToast(message, type = 'info') {
    const toast = document.createElement('div');
    toast.style.cssText = `
        position: fixed;
        top: 20px;
        left: 50%;
        transform: translateX(-50%);
        background: ${type === 'success' ? 'var(--success-color)' : 'var(--warning-color)'};
        color: white;
        padding: 1rem 2rem;
        border-radius: var(--border-radius);
        box-shadow: var(--box-shadow);
        z-index: 10000;
        animation: slideDown 0.3s ease;
    `;
    toast.textContent = message;

    document.body.appendChild(toast);

    setTimeout(() => {
        toast.remove();
    }, 3000);
}
</script>

<style>
@keyframes slideDown {
    from {
        transform: translateX(-50%) translateY(-100%);
        opacity: 0;
    }
    to {
        transform: translateX(-50%) translateY(0);
        opacity: 1;
    }
}

/* Mobile-specific styles */
@media (max-width: 767px) {
    .swipe-container {
        height: 70vh;
    }

    .desktop-only {
        display: none !important;
    }
}

@media (min-width: 768px) {
    .mobile-only {
        display: none !important;
    }
}
</style>

 include_once 'includes/footer.php'; 

5. Update Header for PWA

Update includes/header.php to include PWA meta tags:

<!-- Add these meta tags in the <head> section -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#764ba2">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="MateFinder">
<link rel="apple-touch-icon" href="/assets/icons/icon-192x192.png">
<link rel="manifest" href="/manifest.json">

<!-- Preload critical resources -->
<link rel="preload" href="/assets/css/responsive.css" as="style">
<link rel="preload" href="/assets/js/app.js" as="script">

<!-- CSS -->
<link rel="stylesheet" href="/assets/css/responsive.css">

<!-- Update navigation for mobile -->
<nav class="navbar">
    <div class="container">
        <div class="nav-content">
            <a href="index.php" class="logo">🔥 MateFinder</a>

            <button class="menu-toggle" id="menu-toggle">
                <span></span>
                <span></span>
                <span></span>
            </button>

            <div class="nav-links" id="nav-links">
                 if(isset($_SESSION['user_id'])): 
                     if ($user_subscription && $user_subscription['is_active']): 
                        <span class="premium-badge desktop-only">
                            ⭐  echo $user_subscription['plan_name']; 
                        </span>
                     endif; 
                    <a href="discover.php">Discover</a>
                    <a href="search.php">Search</a>
                    <a href="matches.php">Matches</a>
                    <a href="messages.php" style="position: relative;">
                        Messages

                        $unread_count = get_unread_message_count($_SESSION['user_id'], $conn);
                        if ($unread_count > 0): 
                            <span class="notification-badge"> echo $unread_count; </span>
                         endif; 
                    </a>
                     if (!$user_subscription || !$user_subscription['is_active']): 
                        <a href="premium.php" class="btn desktop-only" style="background: #ff6b6b;">
                            ⭐ Go Premium
                        </a>
                     endif; 
                    <a href="dashboard.php">Dashboard</a>
                    <a href="logout.php" class="btn">Logout</a>
                 else: 
                    <a href="login.php">Login</a>
                    <a href="register.php" class="btn">Find Your Match</a>
                 endif; 
            </div>
        </div>
    </div>
</nav>

<!-- Mobile menu JavaScript -->
<script>
document.getElementById('menu-toggle').addEventListener('click', function() {
    document.getElementById('nav-links').classList.toggle('active');
});
</script>

What We've Accomplished in Part 9

Boom! We've transformed our dating site into a mobile-first, app-like experience:

  1. Responsive Design - Flawless experience on all devices
  2. Progressive Web App - Installable app with offline capabilities
  3. Mobile-Optimized UI - Touch-friendly interfaces and gestures
  4. Service Worker - Offline functionality and background sync
  5. App-like Navigation - Bottom navigation for mobile
  6. Swipe Interactions - Tinder-like swipe experience
  7. Performance Optimizations - Fast loading and smooth animations

Testing Your Mobile Experience

  1. Test on multiple devices - Phones, tablets, different screen sizes
  2. Check PWA installation - Add to home screen functionality
  3. Test offline mode - Service worker caching
  4. Verify touch interactions - Swipe gestures and button taps
  5. Check performance - Lighthouse audits for PWA features

What's Coming in Part 10

Current Status: Your dating site is now a fully-featured mobile app! It's like giving your website a smartphone makeover - suddenly it works perfectly wherever love might strike.


Pro Tip: Use Chrome DevTools device emulation to test different screen sizes. And remember: the best mobile experience is one where users don't even realize they're not using a native app. Our PWA gets pretty close to that ideal!

Building a Dating Website in PHP - Part 10: Launch Day! 🚀

Welcome to the grand finale, you deployment dynamo! We've built an incredible dating website over 9 parts. Now it's time to launch this baby into the world and make sure it stays secure, fast, and successful. This is where we go from "cool project" to "actual business"!

What We're Building in Part 10

1. Production Deployment Setup

1.1 Server Configuration

Create deploy/setup-server.sh - our server setup script:

#!/bin/bash
# deploy/setup-server.sh

echo "🚀 Starting MateFinder Server Setup..."

# Update system
apt update && apt upgrade -y

# Install required packages
apt install -y nginx mysql-server php8.1-fpm php8.1-mysql php8.1-curl php8.1-gd php8.1-mbstring php8.1-xml php8.1-zip php8.1-intl nodejs npm certbot python3-certbot-nginx

# Create application directory
mkdir -p /var/www/matefinder
chown -R www-data:www-data /var/www/matefinder

# Configure MySQL
mysql_secure_installation

# Create database and user
mysql -e "CREATE DATABASE matefinder CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -e "CREATE USER 'matefinder_user'@'localhost' IDENTIFIED BY '$(openssl rand -base64 32)';"
mysql -e "GRANT ALL PRIVILEGES ON matefinder.* TO 'matefinder_user'@'localhost';"
mysql -e "FLUSH PRIVILEGES;"

echo "✅ Server setup complete!"

1.2 Nginx Configuration

Create deploy/nginx.conf - our production web server config:

# deploy/nginx.conf
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    root /var/www/matefinder;
    index index.php index.html;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;
    add_header Content-Security-Policy "default-src 'self' https: data: 'unsafe-inline' 'unsafe-eval';" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types
        application/atom+xml
        application/geo+json
        application/javascript
        application/x-javascript
        application/json
        application/ld+json
        application/manifest+json
        application/rdf+xml
        application/rss+xml
        application/xhtml+xml
        application/xml
        font/eot
        font/otf
        font/ttf
        image/svg+xml
        text/css
        text/javascript
        text/plain
        text/xml;

    # Cache static assets
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|pdf|txt)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # PHP handling
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Block sensitive files
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }

    location ~ /(config|includes|deploy|sql|\.env) {
        deny all;
        access_log off;
        log_not_found off;
    }

    # PWA service worker
    location /sw.js {
        add_header Cache-Control "no-cache";
        proxy_cache_background_update on;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    }

    # Main application
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # API endpoints
    location /api/ {
        try_files $uri $uri/ /api/index.php?$query_string;
    }

    # WebSocket proxy for real-time features
    location /ws/ {
        proxy_pass http://localhost:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# HTTPS redirect
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

1.3 PHP Production Configuration

Create config/production.php - our production PHP settings:


// config/production.php

// Error reporting - be less verbose in production
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php/matefinder-errors.log');

// Memory and execution limits
ini_set('memory_limit', '256M');
ini_set('max_execution_time', 30);
ini_set('max_input_time', 30);

// Session security
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.use_strict_mode', 1);
ini_set('session.cookie_samesite', 'Strict');

// File upload limits
ini_set('upload_max_filesize', '10M');
ini_set('post_max_size', '10M');
ini_set('max_file_uploads', 20);

// Performance optimizations
ini_set('opcache.enable', 1);
ini_set('opcache.memory_consumption', 256);
ini_set('opcache.max_accelerated_files', 20000);
ini_set('opcache.validate_timestamps', 0); // Set to 1 in development

// Database configuration for production
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
define('DB_NAME', getenv('DB_NAME') ?: 'matefinder');
define('DB_USER', getenv('DB_USER') ?: 'matefinder_user');
define('DB_PASS', getenv('DB_PASS') ?: 'your_secure_password_here');

// Stripe configuration
define('STRIPE_PUBLIC_KEY', getenv('STRIPE_PUBLIC_KEY') ?: 'pk_live_...');
define('STRIPE_SECRET_KEY', getenv('STRIPE_SECRET_KEY') ?: 'sk_live_...');

// Email configuration
define('SMTP_HOST', getenv('SMTP_HOST') ?: 'smtp.mailgun.org');
define('SMTP_USER', getenv('SMTP_USER') ?: 'your_smtp_user');
define('SMTP_PASS', getenv('SMTP_PASS') ?: 'your_smtp_password');

// File storage
define('UPLOAD_PATH', '/var/www/matefinder/uploads/');
define('BACKUP_PATH', '/var/backups/matefinder/');

// Security keys
define('ENCRYPTION_KEY', getenv('ENCRYPTION_KEY') ?: 'your-32-character-encryption-key-here');
define('JWT_SECRET', getenv('JWT_SECRET') ?: 'your-jwt-secret-key-here');

// Feature flags
define('MAINTENANCE_MODE', false);
define('REGISTRATION_ENABLED', true);
define('EMAIL_VERIFICATION_REQUIRED', true);

2. Security Hardening

2.1 Security Helper Functions

Create includes/security.php - our security fortress:


// includes/security.php

class Security {

    // Prevent SQL injection
    public static function sanitizeSQL($input, $conn) {
        if (is_array($input)) {
            return array_map(function($item) use ($conn) {
                return self::sanitizeSQL($item, $conn);
            }, $input);
        }

        if ($conn instanceof PDO) {
            return $conn->quote($input);
        }

        return addslashes($input);
    }

    // Prevent XSS attacks
    public static function sanitizeHTML($input) {
        if (is_array($input)) {
            return array_map([self::class, 'sanitizeHTML'], $input);
        }

        return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    }

    // Validate file uploads
    public static function validateFileUpload($file, $allowedTypes = ['image/jpeg', 'image/png', 'image/gif']) {
        $errors = [];

        // Check for upload errors
        if ($file['error'] !== UPLOAD_ERR_OK) {
            $errors[] = "File upload failed with error code: " . $file['error'];
            return [false, $errors];
        }

        // Check file size (max 10MB)
        if ($file['size'] > 10 * 1024 * 1024) {
            $errors[] = "File size exceeds 10MB limit";
        }

        // Check MIME type
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mime = finfo_file($finfo, $file['tmp_name']);
        finfo_close($finfo);

        if (!in_array($mime, $allowedTypes)) {
            $errors[] = "Invalid file type. Allowed: " . implode(', ', $allowedTypes);
        }

        // Check file extension
        $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif'];
        if (!in_array($extension, $allowedExtensions)) {
            $errors[] = "Invalid file extension";
        }

        // Check for malicious content in images
        if (strpos($mime, 'image/') === 0) {
            $imageInfo = getimagesize($file['tmp_name']);
            if ($imageInfo === false) {
                $errors[] = "File is not a valid image";
            }
        }

        return [empty($errors), $errors];
    }

    // Generate CSRF token
    public static function generateCSRFToken() {
        if (empty($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
        return $_SESSION['csrf_token'];
    }

    // Validate CSRF token
    public static function validateCSRFToken($token) {
        if (empty($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $token)) {
            throw new Exception("Invalid CSRF token");
        }
        return true;
    }

    // Rate limiting
    public static function checkRateLimit($key, $maxAttempts = 5, $timeWindow = 900) { // 15 minutes
        $redis = new Redis();
        $redis->connect('127.0.0.1', 6379);

        $current = $redis->get($key);
        if ($current && $current >= $maxAttempts) {
            return false;
        }

        $redis->multi();
        $redis->incr($key);
        $redis->expire($key, $timeWindow);
        $redis->exec();

        return true;
    }

    // Password strength validation
    public static function validatePasswordStrength($password) {
        $errors = [];

        if (strlen($password) < 8) {
            $errors[] = "Password must be at least 8 characters long";
        }

        if (!preg_match('/[A-Z]/', $password)) {
            $errors[] = "Password must contain at least one uppercase letter";
        }

        if (!preg_match('/[a-z]/', $password)) {
            $errors[] = "Password must contain at least one lowercase letter";
        }

        if (!preg_match('/[0-9]/', $password)) {
            $errors[] = "Password must contain at least one number";
        }

        if (!preg_match('/[^A-Za-z0-9]/', $password)) {
            $errors[] = "Password must contain at least one special character";
        }

        // Check against common passwords
        $commonPasswords = ['password', '123456', 'qwerty', 'letmein'];
        if (in_array(strtolower($password), $commonPasswords)) {
            $errors[] = "Password is too common";
        }

        return $errors;
    }

    // Encrypt sensitive data
    public static function encrypt($data, $key = null) {
        $key = $key ?: ENCRYPTION_KEY;
        $iv = random_bytes(16);
        $encrypted = openssl_encrypt($data, 'AES-256-CBC', $key, 0, $iv);
        return base64_encode($iv . $encrypted);
    }

    // Decrypt sensitive data
    public static function decrypt($data, $key = null) {
        $key = $key ?: ENCRYPTION_KEY;
        $data = base64_decode($data);
        $iv = substr($data, 0, 16);
        $encrypted = substr($data, 16);
        return openssl_decrypt($encrypted, 'AES-256-CBC', $key, 0, $iv);
    }

    // Log security events
    public static function logSecurityEvent($event, $userId = null, $ip = null) {
        $ip = $ip ?: $_SERVER['REMOTE_ADDR'];
        $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown';

        $logEntry = [
            'timestamp' => date('Y-m-d H:i:s'),
            'event' => $event,
            'user_id' => $userId,
            'ip_address' => $ip,
            'user_agent' => $userAgent,
            'request_uri' => $_SERVER['REQUEST_URI'] ?? ''
        ];

        file_put_contents(
            '/var/log/matefinder/security.log',
            json_encode($logEntry) . PHP_EOL,
            FILE_APPEND | LOCK_EX
        );
    }
}

2.2 Input Validation Middleware

Create includes/validation.php - our input validation system:


// includes/validation.php

class Validator {

    private $errors = [];
    private $data;

    public function __construct($data) {
        $this->data = $data;
    }

    public function validate($rules) {
        foreach ($rules as $field => $ruleSet) {
            $value = $this->getValue($field);
            $rules = explode('|', $ruleSet);

            foreach ($rules as $rule) {
                $this->applyRule($field, $value, $rule);
            }
        }

        return empty($this->errors);
    }

    public function getErrors() {
        return $this->errors;
    }

    private function getValue($field) {
        return $this->data[$field] ?? null;
    }

    private function applyRule($field, $value, $rule) {
        $params = [];

        if (strpos($rule, '😂 !== false) {
            list($rule, $param) = explode(':', $rule, 2);
            $params = explode(',', $param);
        }

        $method = 'validate' . ucfirst($rule);

        if (method_exists($this, $method)) {
            if (!$this->$method($field, $value, $params)) {
                $this->addError($field, $rule, $params);
            }
        }
    }

    private function addError($field, $rule, $params) {
        $messages = [
            'required' => "The $field field is required",
            'email' => "The $field must be a valid email address",
            'min' => "The $field must be at least {$params[0]} characters",
            'max' => "The $field may not be greater than {$params[0]} characters",
            'numeric' => "The $field must be a number",
            'url' => "The $field must be a valid URL",
            'date' => "The $field must be a valid date",
            'in' => "The $field must be one of: " . implode(', ', $params)
        ];

        $this->errors[$field][] = $messages[$rule] ?? "The $field field is invalid";
    }

    // Validation rules
    private function validateRequired($field, $value, $params) {
        return !empty(trim($value ?? ''));
    }

    private function validateEmail($field, $value, $params) {
        return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
    }

    private function validateMin($field, $value, $params) {
        return strlen($value ?? '') >= (int)$params[0];
    }

    private function validateMax($field, $value, $params) {
        return strlen($value ?? '') <= (int)$params[0];
    }

    private function validateNumeric($field, $value, $params) {
        return is_numeric($value);
    }

    private function validateUrl($field, $value, $params) {
        return filter_var($value, FILTER_VALIDATE_URL) !== false;
    }

    private function validateDate($field, $value, $params) {
        return strtotime($value) !== false;
    }

    private function validateIn($field, $value, $params) {
        return in_array($value, $params);
    }
}

3. Performance Optimization

3.1 Caching System

Create includes/cache.php - our performance booster:


// includes/cache.php

class Cache {
    private $redis;
    private $enabled;

    public function __construct() {
        $this->enabled = extension_loaded('redis');

        if ($this->enabled) {
            $this->redis = new Redis();
            try {
                $this->redis->connect('127.0.0.1', 6379, 2.5);
                $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
            } catch (Exception $e) {
                $this->enabled = false;
                error_log("Redis connection failed: " . $e->getMessage());
            }
        }
    }

    public function get($key) {
        if (!$this->enabled) return false;

        try {
            return $this->redis->get($key);
        } catch (Exception $e) {
            error_log("Cache get error: " . $e->getMessage());
            return false;
        }
    }

    public function set($key, $value, $ttl = 3600) {
        if (!$this->enabled) return false;

        try {
            return $this->redis->setex($key, $ttl, $value);
        } catch (Exception $e) {
            error_log("Cache set error: " . $e->getMessage());
            return false;
        }
    }

    public function delete($key) {
        if (!$this->enabled) return false;

        try {
            return $this->redis->del($key);
        } catch (Exception $e) {
            error_log("Cache delete error: " . $e->getMessage());
            return false;
        }
    }

    public function remember($key, $ttl, $callback) {
        $cached = $this->get($key);

        if ($cached !== false) {
            return $cached;
        }

        $value = $callback();
        $this->set($key, $value, $ttl);

        return $value;
    }

    // Cache user data
    public function cacheUser($userId, $userData) {
        $key = "user:$userId";
        return $this->set($key, $userData, 1800); // 30 minutes
    }

    public function getUser($userId) {
        $key = "user:$userId";
        return $this->get($key);
    }

    // Cache match results
    public function cacheMatches($userId, $matches) {
        $key = "matches:$userId";
        return $this->set($key, $matches, 900); // 15 minutes
    }

    public function getMatches($userId) {
        $key = "matches:$userId";
        return $this->get($key);
    }
}

3.2 Database Optimization

Create deploy/database-optimization.sql - our performance SQL:

-- deploy/database-optimization.sql

-- Add missing indexes for better performance
ALTER TABLE users ADD INDEX idx_active_completed_created (is_active, profile_completed, profile_created);
ALTER TABLE user_likes ADD INDEX idx_liker_liked_date (liker_id, liked_id, like_date);
ALTER TABLE messages ADD INDEX idx_conversation_read (conversation_id, is_read, created_at);
ALTER TABLE profile_views ADD INDEX idx_viewed_viewer_date (viewed_user_id, viewer_id, view_date);

-- Optimize tables
OPTIMIZE TABLE users, user_likes, user_matches, messages, conversations, profile_views;

-- Create views for common queries
CREATE VIEW user_stats AS
SELECT 
    u.id as user_id,
    u.first_name,
    u.last_name,
    COUNT(DISTINCT ul.id) as likes_received,
    COUNT(DISTINCT um.id) as matches_count,
    COUNT(DISTINCT pv.id) as profile_views,
    (SELECT COUNT(*) FROM messages m 
     JOIN conversations c ON m.conversation_id = c.id 
     WHERE (c.user1_id = u.id OR c.user2_id = u.id) 
     AND m.sender_id != u.id 
     AND m.is_read = 0) as unread_messages
FROM users u
LEFT JOIN user_likes ul ON u.id = ul.liked_id
LEFT JOIN user_matches um ON (u.id = um.user1_id OR u.id = um.user2_id)
LEFT JOIN profile_views pv ON u.id = pv.viewed_user_id
WHERE u.is_active = 1
GROUP BY u.id;

-- Create stored procedure for match calculation
DELIMITER //
CREATE PROCEDURE CalculateUserMatches(IN user_id INT)
BEGIN
    SELECT 
        u.*,
        TIMESTAMPDIFF(YEAR, u.date_of_birth, CURDATE()) as age,
        (SELECT COUNT(*) FROM user_photos up WHERE up.user_id = u.id) as photo_count,
        (
            -- Calculate compatibility score
            (CASE WHEN u.relationship_goal = (SELECT relationship_goal FROM users WHERE id = user_id) THEN 20 ELSE 0 END) +
            (CASE WHEN ABS(TIMESTAMPDIFF(YEAR, u.date_of_birth, (SELECT date_of_birth FROM users WHERE id = user_id))) <= 5 THEN 15 ELSE 0 END) +
            (SELECT COUNT(*) FROM (
                SELECT TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(u.interests, ',', n.n), ',', -1)) as interest
                FROM (SELECT 1 n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) n
                WHERE n.n <= 1 + (LENGTH(u.interests) - LENGTH(REPLACE(u.interests, ',', '')))
                AND TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(u.interests, ',', n.n), ',', -1)) IN (
                    SELECT TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(interests, ',', n.n), ',', -1))
                    FROM users, (SELECT 1 n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) n
                    WHERE id = user_id
                    AND n.n <= 1 + (LENGTH(interests) - LENGTH(REPLACE(interests, ',', '')))
                )
            ) common_interests) * 5
        ) as compatibility_score
    FROM users u
    WHERE u.id != user_id
    AND u.is_active = 1
    AND u.profile_completed = 1
    AND u.id NOT IN (SELECT liked_id FROM user_likes WHERE liker_id = user_id)
    HAVING compatibility_score > 30
    ORDER BY compatibility_score DESC
    LIMIT 50;
END //
DELIMITER ;

4. SEO and Analytics

4.1 SEO Optimization

Create includes/seo.php - our search engine helper:


// includes/seo.php

class SEO {

    public static function generateMetaTags($pageData) {
        $defaults = [
            'title' => 'MateFinder - Find Your Perfect Match',
            'description' => 'Join MateFinder to meet new people and find meaningful connections. Free sign up!',
            'keywords' => 'dating, relationships, meet people, singles, matchmaking',
            'canonical' => self::getCanonicalUrl(),
            'og_image' => '/assets/images/og-default.jpg',
            'og_type' => 'website'
        ];

        $data = array_merge($defaults, $pageData);

        $metaTags = [
            '<title>' . htmlspecialchars($data['title']) . '</title>',
            '<meta name="description" content="' . htmlspecialchars($data['description']) . '">',
            '<meta name="keywords" content="' . htmlspecialchars($data['keywords']) . '">',
            '<link rel="canonical" href="' . htmlspecialchars($data['canonical']) . '">',

            // Open Graph
            '<meta property="og:title" content="' . htmlspecialchars($data['title']) . '">',
            '<meta property="og:description" content="' . htmlspecialchars($data['description']) . '">',
            '<meta property="og:url" content="' . htmlspecialchars($data['canonical']) . '">',
            '<meta property="og:image" content="' . htmlspecialchars($data['og_image']) . '">',
            '<meta property="og:type" content="' . htmlspecialchars($data['og_type']) . '">',
            '<meta property="og:site_name" content="MateFinder">',

            // Twitter Card
            '<meta name="twitter:card" content="summary_large_image">',
            '<meta name="twitter:title" content="' . htmlspecialchars($data['title']) . '">',
            '<meta name="twitter:description" content="' . htmlspecialchars($data['description']) . '">',
            '<meta name="twitter:image" content="' . htmlspecialchars($data['og_image']) . '">',

            // Structured Data
            self::generateStructuredData($data)
        ];

        return implode(PHP_EOL, $metaTags);
    }

    private static function getCanonicalUrl() {
        $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
        $host = $_SERVER['HTTP_HOST'];
        $uri = $_SERVER['REQUEST_URI'];

        return $protocol . '://' . $host . $uri;
    }

    private static function generateStructuredData($data) {
        $structuredData = [
            '@context' => 'https://schema.org',
            '@type' => 'WebApplication',
            'name' => 'MateFinder',
            'description' => $data['description'],
            'url' => self::getCanonicalUrl(),
            'applicationCategory' => 'DatingApplication',
            'operatingSystem' => 'Any',
            'permissions' => 'geolocation',
            'offers' => [
                '@type' => 'Offer',
                'price' => '0',
                'priceCurrency' => 'USD'
            ]
        ];

        return '<script type="application/ld+json">' . json_encode($structuredData) . '</script>';
    }

    public static function generateSitemap($conn) {
        $baseUrl = 'https://yourdomain.com';
        $pages = [
            '/' => 'daily',
            '/login.php' => 'monthly',
            '/register.php' => 'monthly',
            '/discover.php' => 'always',
            '/search.php' => 'always',
            '/premium.php' => 'monthly'
        ];

        // Get active user profiles for sitemap
        $usersQuery = "SELECT id, profile_created FROM users WHERE is_active = 1 AND profile_completed = 1";
        $usersStmt = $conn->prepare($usersQuery);
        $usersStmt->execute();
        $users = $usersStmt->fetchAll(PDO::FETCH_ASSOC);

        $sitemap = '<?xml version="1.0" encoding="UTF-8"';
        $sitemap .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';

        foreach ($pages as $page => $changefreq) {
            $sitemap .= '<url>';
            $sitemap .= '<loc>' . htmlspecialchars($baseUrl . $page) . '</loc>';
            $sitemap .= '<changefreq>' . $changefreq . '</changefreq>';
            $sitemap .= '<priority>0.8</priority>';
            $sitemap .= '</url>';
        }

        foreach ($users as $user) {
            $sitemap .= '<url>';
            $sitemap .= '<loc>' . htmlspecialchars($baseUrl . '/profile.php?user_id=' . $user['id']) . '</loc>';
            $sitemap .= '<lastmod>' . date('c', strtotime($user['profile_created'])) . '</lastmod>';
            $sitemap .= '<changefreq>weekly</changefreq>';
            $sitemap .= '<priority>0.6</priority>';
            $sitemap .= '</url>';
        }

        $sitemap .= '</urlset>';

        return $sitemap;
    }
}

4.2 Analytics Integration

Create includes/analytics.php - our tracking system:


// includes/analytics.php

class Analytics {

    public static function trackPageView($page, $userId = null) {
        $data = [
            'v' => 1,
            'tid' => 'UA-XXXXXXXXX-X', // Your Google Analytics ID
            'cid' => self::getClientId($userId),
            't' => 'pageview',
            'dh' => $_SERVER['HTTP_HOST'],
            'dp' => $page,
            'dt' => self::getPageTitle($page),
            'uid' => $userId,
            'uip' => $_SERVER['REMOTE_ADDR'],
            'ua' => $_SERVER['HTTP_USER_AGENT']
        ];

        self::sendToGoogleAnalytics($data);
    }

    public static function trackEvent($category, $action, $label = null, $value = null, $userId = null) {
        $data = [
            'v' => 1,
            'tid' => 'UA-XXXXXXXXX-X',
            'cid' => self::getClientId($userId),
            't' => 'event',
            'ec' => $category,
            'ea' => $action,
            'el' => $label,
            'ev' => $value,
            'uid' => $userId
        ];

        self::sendToGoogleAnalytics($data);

        // Also track in our database
        self::trackEventInDB($category, $action, $label, $value, $userId);
    }

    private static function getClientId($userId) {
        if (empty($_COOKIE['_ga'])) {
            return uniqid('', true);
        }

        preg_match('/GA1\.\d+\.(.+)/', $_COOKIE['_ga'], $matches);
        return $matches[1] ?? uniqid('', true);
    }

    private static function getPageTitle($page) {
        $titles = [
            '/discover.php' => 'Discover Matches - MateFinder',
            '/messages.php' => 'Messages - MateFinder',
            '/profile.php' => 'Profile - MateFinder',
            '/premium.php' => 'Go Premium - MateFinder'
        ];

        return $titles[$page] ?? 'MateFinder - Find Your Perfect Match';
    }

    private static function sendToGoogleAnalytics($data) {
        $url = 'https://www.google-analytics.com/collect';
        $options = [
            'http' => [
                'header' => "Content-type: application/x-www-form-urlencoded\r\n",
                'method' => 'POST',
                'content' => http_build_query($data),
                'timeout' => 1.0
            ]
        ];

        // Send asynchronously
        register_shutdown_function(function() use ($url, $options) {
            @file_get_contents($url, false, stream_context_create($options));
        });
    }

    private static function trackEventInDB($category, $action, $label, $value, $userId) {
        global $conn;

        $query = "INSERT INTO analytics_events (user_id, category, action, label, value, ip_address, user_agent, created_at) 
                  VALUES (:user_id, :category, :action, :label, :value, :ip, :ua, NOW())";

        $stmt = $conn->prepare($query);
        $stmt->execute([
            ':user_id' => $userId,
            ':category' => $category,
            ':action' => $action,
            ':label' => $label,
            ':value' => $value,
            ':ip' => $_SERVER['REMOTE_ADDR'],
            ':ua' => $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown'
        ]);
    }

    public static function getDashboardStats($conn) {
        $stats = [];

        // Total users
        $query = "SELECT COUNT(*) as total_users FROM users WHERE is_active = 1";
        $stmt = $conn->prepare($query);
        $stmt->execute();
        $stats['total_users'] = $stmt->fetchColumn();

        // Active today
        $query = "SELECT COUNT(DISTINCT user_id) as active_today FROM user_sessions WHERE last_activity >= DATE_SUB(NOW(), INTERVAL 1 DAY)";
        $stmt = $conn->prepare($query);
        $stmt->execute();
        $stats['active_today'] = $stmt->fetchColumn();

        // New registrations today
        $query = "SELECT COUNT(*) as new_today FROM users WHERE profile_created >= CURDATE()";
        $stmt = $conn->prepare($query);
        $stmt->execute();
        $stats['new_today'] = $stmt->fetchColumn();

        // Total matches
        $query = "SELECT COUNT(*) as total_matches FROM user_matches WHERE is_active = 1";
        $stmt = $conn->prepare($query);
        $stmt->execute();
        $stats['total_matches'] = $stmt->fetchColumn();

        // Premium subscribers
        $query = "SELECT COUNT(*) as premium_users FROM user_subscriptions WHERE status = 'active'";
        $stmt = $conn->prepare($query);
        $stmt->execute();
        $stats['premium_users'] = $stmt->fetchColumn();

        return $stats;
    }
}

5. Launch Checklist

Create LAUNCH_CHECKLIST.md - our final pre-flight check:

# MateFinder Launch Checklist ✅

## Pre-Launch (1 week before)
- <input type="checkbox"> Purchase domain and set up DNS
- <input type="checkbox"> Set up production server (VPS/Cloud)
- <input type="checkbox"> Configure SSL certificate
- <input type="checkbox"> Set up email service (SendGrid/Mailgun)
- <input type="checkbox"> Configure Stripe for payments
- <input type="checkbox"> Set up backup system
- <input type="checkbox"> Configure monitoring (UptimeRobot)

## Security
- <input type="checkbox"> Change all default passwords
- <input type="checkbox"> Set up firewall (UFW)
- <input type="checkbox"> Configure fail2ban
- <input type="checkbox"> Set up file permissions
- <input type="checkbox"> Remove unused services
- <input type="checkbox"> Set up security headers
- <input type="checkbox"> Configure CSRF protection
- <input type="checkbox"> Set up rate limiting
- <input type="checkbox"> Enable SQL injection protection
- <input type="checkbox"> Configure XSS protection

## Performance
- <input type="checkbox"> Enable OPcache
- <input type="checkbox"> Set up Redis caching
- <input type="checkbox"> Configure gzip compression
- <input type="checkbox"> Optimize images
- <input type="checkbox"> Minify CSS/JS
- <input type="checkbox"> Set up CDN (Cloudflare)
- <input type="checkbox"> Configure browser caching
- <input type="checkbox"> Database optimization
- <input type="checkbox"> Load testing

## SEO & Marketing
- <input type="checkbox"> Set up Google Analytics
- <input type="checkbox"> Configure Google Search Console
- <input type="checkbox"> Set up social media accounts
- <input type="checkbox"> Create Facebook Pixel
- <input type="checkbox"> Set up email marketing (Mailchimp)
- <input type="checkbox"> Create landing pages
- <input type="checkbox"> Set up referral program
- <input type="checkbox"> Prepare press kit

## Legal
- <input type="checkbox"> Terms of Service
- <input type="checkbox"> Privacy Policy
- <input type="checkbox"> Cookie Policy
- <input type="checkbox"> GDPR compliance
- <input type="checkbox"> Age verification
- <input type="checkbox"> Content moderation policy
- <input type="checkbox"> Payment terms

## Testing
- <input type="checkbox"> Cross-browser testing
- <input type="checkbox"> Mobile responsiveness
- <input type="checkbox"> Payment processing
- <input type="checkbox"> Email delivery
- <input type="checkbox"> File uploads
- <input type="checkbox"> User registration flow
- <input type="checkbox"> Messaging system
- <input type="checkbox"> Search functionality
- <input type="checkbox"> Error handling

## Launch Day
- <input type="checkbox"> Final backup
- <input type="checkbox"> DNS propagation check
- <input type="checkbox"> SSL certificate verification
- <input type="checkbox"> Monitor server performance
- <input type="checkbox"> Test all critical paths
- <input type="checkbox"> Enable analytics
- <input type="checkbox"> Announce on social media
- <input type="checkbox"> Send launch email to waitlist

## Post-Launch
- <input type="checkbox"> Monitor error logs
- <input type="checkbox"> Track user feedback
- <input type="checkbox"> Monitor server metrics
- <input type="checkbox"> Regular backups
- <input type="checkbox"> Security updates
- <input type="checkbox"> Performance optimization
- <input type="checkbox"> User support system

6. Final Deployment Script

Create deploy/deploy.sh - our one-click deployment:

#!/bin/bash
# deploy/deploy.sh

set -e

echo "🚀 Starting MateFinder Deployment..."

# Configuration
APP_DIR="/var/www/matefinder"
BACKUP_DIR="/var/backups/matefinder"
DATE=$(date +%Y%m%d_%H%M%S)

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

# Functions
log() {
    echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}"
}

warn() {
    echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}"
}

error() {
    echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}"
    exit 1
}

# Check if running as root
if [ "$EUID" -ne 0 ]; then
    error "Please run as root"
fi

# Backup current version
backup() {
    log "Backing up current version..."
    tar -czf "$BACKUP_DIR/backup_$DATE.tar.gz" -C "$APP_DIR" . || error "Backup failed"
    log "Backup created: $BACKUP_DIR/backup_$DATE.tar.gz"
}

# Deploy new version
deploy() {
    log "Deploying new version..."

    # Create app directory if it doesn't exist
    mkdir -p "$APP_DIR"

    # Copy new files
    cp -r ./* "$APP_DIR/" || error "File copy failed"

    # Set permissions
    chown -R www-data:www-data "$APP_DIR"
    chmod -R 755 "$APP_DIR"
    chmod -R 644 "$APP_DIR/config/production.php"

    # Create necessary directories
    mkdir -p "$APP_DIR/uploads"
    mkdir -p "$APP_DIR/logs"
    chown -R www-data:www-data "$APP_DIR/uploads"
    chown -R www-data:www-data "$APP_DIR/logs"

    log "Files deployed successfully"
}

# Database migration
migrate_db() {
    log "Running database migrations..."
    mysql -u root -p"$DB_PASSWORD" matefinder < "$APP_DIR/deploy/database-optimization.sql" || warn "Database migration may have issues"
    log "Database migrations completed"
}

# Clear caches
clear_caches() {
    log "Clearing caches..."

    # Clear OPcache
    if [ -f /etc/php/8.1/fpm/php.ini ]; then
        service php8.1-fpm reload
    fi

    # Clear Redis cache
    redis-cli FLUSHALL || warn "Redis cache clear failed"

    log "Caches cleared"
}

# Restart services
restart_services() {
    log "Restarting services..."

    service nginx reload || error "Nginx reload failed"
    service php8.1-fpm restart || error "PHP-FPM restart failed"
    service mysql restart || error "MySQL restart failed"

    # Restart WebSocket server
    pm2 restart matefinder-ws || warn "WebSocket server restart failed"

    log "Services restarted"
}

# Health check
health_check() {
    log "Performing health check..."

    # Check if website is responding
    response=$(curl -s -o /dev/null -w "%{http_code}" https://yourdomain.com/)
    if [ "$response" -ne 200 ]; then
        error "Health check failed: HTTP $response"
    fi

    # Check database connection
    mysql -u root -p"$DB_PASSWORD" -e "SELECT 1" matefinder || error "Database connection failed"

    log "Health check passed"
}

# Main deployment process
main() {
    log "Starting deployment process..."

    # Get database password
    read -sp "Enter MySQL root password: " DB_PASSWORD
    echo

    backup
    deploy
    migrate_db
    clear_caches
    restart_services
    sleep 5  # Wait for services to start
    health_check

    log "🎉 Deployment completed successfully!"
    log "📊 Check analytics: https://analytics.google.com"
    log "🚨 Monitor errors: tail -f /var/log/nginx/error.log"
    log "👥 User feedback: Check your support email"
}

# Run deployment
main

What We've Accomplished in Part 10

We've built a complete production-ready deployment system:

  1. Production Server Setup - Secure, optimized server configuration
  2. Security Hardening - Comprehensive security measures
  3. Performance Optimization - Caching, database optimization, and CDN
  4. SEO & Analytics - Search engine optimization and user tracking
  5. Monitoring & Maintenance - Logging, backups, and health checks
  6. Deployment Automation - One-click deployment scripts

Your Dating Website is Now Complete! 🎉

Over these 10 parts, we've built an incredible, full-featured dating website:

What We Built:

Key Features:

Technology Stack:

Next Steps:

  1. Launch Your Site - Use the deployment scripts to go live
  2. Market Your Platform - Use social media and dating forums
  3. Gather Feedback - Listen to your users and iterate
  4. Scale Up - Monitor performance and scale as needed
  5. Add Features - Continue improving based on user needs

Congratulations! You've built a complete, production-ready dating website from scratch. You're now ready to launch and start connecting people! 🚀

Remember: The most important feature of any dating site is the people using it. Focus on building a great community, and the rest will follow.


Final Pro Tip: Launch with a "Founders Circle" - offer the first 100 users lifetime premium features for free. They'll become your most loyal users and best evangelists. Now go change the world of dating!

Back to ChameleonSoftwareOnline.com