'; } function verify_csrf(): void { $token = $_POST['csrf_token'] ?? ''; if (!$token || !hash_equals($_SESSION[CSRF_KEY] ?? '', $token)) { http_response_code(419); exit('Security check failed. Please go back and try again.'); } } function flash(string $type, string $message): void { $_SESSION['flash'][] = ['type' => $type, 'message' => $message]; } function get_flashes(): array { $items = $_SESSION['flash'] ?? []; unset($_SESSION['flash']); return $items; } function is_post(): bool { return strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST'; } function slugify(string $text): string { $text = strtolower(trim($text)); $text = preg_replace('/[^a-z0-9]+/i', '-', $text) ?? ''; $text = trim($text, '-'); return $text ?: 'item'; } function format_price(float|int|string|null $price): string { if ($price === null || $price === '') return 'Contact for price'; return 'Rs ' . number_format((float)$price, 0); } function time_ago(string $datetime): string { $ts = strtotime($datetime); if (!$ts) return e($datetime); $diff = time() - $ts; if ($diff < 60) return 'just now'; if ($diff < 3600) return floor($diff / 60) . ' min ago'; if ($diff < 86400) return floor($diff / 3600) . ' hr ago'; if ($diff < 2592000) return floor($diff / 86400) . ' day ago'; return date('d M Y', $ts); } function current_user(): ?array { static $user = null; static $loaded = false; if ($loaded) return $user; $loaded = true; $id = $_SESSION['user_id'] ?? null; if (!$id) return null; $stmt = db()->prepare('SELECT * FROM users WHERE id = ? LIMIT 1'); $stmt->execute([(int)$id]); $user = $stmt->fetch(PDO::FETCH_ASSOC) ?: null; return $user; } function require_login(): void { if (!current_user()) { flash('error', 'Please sign in first.'); redirect('?page=login'); } } function require_admin(): void { $user = current_user(); if (!$user || (int)$user['is_admin'] !== 1) { http_response_code(403); exit('Forbidden'); } } function db(): PDO { static $pdo = null; if ($pdo instanceof PDO) return $pdo; $needsBootstrap = !file_exists(DB_FILE); $pdo = new PDO('sqlite:' . DB_FILE); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $pdo->exec('PRAGMA foreign_keys = ON'); if ($needsBootstrap) { bootstrap_database($pdo); } else { // Safety bootstrap if file exists but tables do not $stmt = $pdo->query("SELECT name FROM sqlite_master WHERE type='table' AND name='users'"); if (!$stmt->fetchColumn()) { bootstrap_database($pdo); } } return $pdo; } function bootstrap_database(PDO $pdo): void { $sql = <<exec($sql); $adminPass = password_hash('admin123', PASSWORD_DEFAULT); $seedUsers = $pdo->prepare('INSERT INTO users (name, email, password_hash, phone, city, is_admin) VALUES (?, ?, ?, ?, ?, ?)'); $seedUsers->execute(['Mix Admin', 'admin@mix.local', $adminPass, '0300-0000000', 'Lahore', 1]); $seedUsers->execute(['Adeel Khan', 'adeel@example.com', password_hash('password123', PASSWORD_DEFAULT), '0311-2223344', 'Karachi', 0]); $seedUsers->execute(['Sara Ahmed', 'sara@example.com', password_hash('password123', PASSWORD_DEFAULT), '0333-9876543', 'Islamabad', 0]); $categories = [ ['Mobiles', 'mobiles', '📱'], ['Cars', 'cars', '🚗'], ['Property for Sale', 'property-sale', '🏠'], ['Property for Rent', 'property-rent', '🏘️'], ['Electronics & Home Appliances', 'electronics-home-appliances', '💻'], ['Bikes', 'bikes', '🏍️'], ['Jobs', 'jobs', '💼'], ['Services', 'services', '🛠️'], ['Furniture & Home Decor', 'furniture-home-decor', '🛋️'], ['Fashion & Beauty', 'fashion-beauty', '👗'], ]; $stmt = $pdo->prepare('INSERT INTO categories (name, slug, icon) VALUES (?, ?, ?)'); foreach ($categories as $cat) { $stmt->execute($cat); } $listingStmt = $pdo->prepare('INSERT INTO listings (user_id, category_id, title, slug, description, price, city, area, image_path, phone, status, views, is_featured) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'); $samples = [ [2, 1, 'iPhone 13 128GB PTA Approved', 'iphone-13-128gb-pta-approved', 'Neat condition, battery health 88%, original box available. No Face ID issue. Serious buyers only.', 144999, 'Karachi', 'Gulshan-e-Iqbal', '', '0311-2223344', 'approved', 32, 1], [3, 2, 'Honda Civic UG 2021', 'honda-civic-ug-2021', 'First owner, genuine condition, smart card available, driven with care. Inspection welcome.', 5295000, 'Islamabad', 'F-10', '', '0333-9876543', 'approved', 17, 1], [2, 3, '5 Marla House for Sale', '5-marla-house-for-sale', 'Near park and mosque, double storey, ideal for family living, water and gas available.', 21500000, 'Lahore', 'Johar Town', '', '0311-2223344', 'approved', 23, 1], [3, 5, 'Dell Latitude i7 16GB / 512GB SSD', 'dell-latitude-i7-16gb-512gb-ssd', 'Office use laptop, clean condition, genuine battery timing, charger included.', 118000, 'Rawalpindi', 'Saddar', '', '0333-9876543', 'approved', 11, 0], [2, 8, 'Graphic Designer Available for Social Media Posts', 'graphic-designer-available-for-social-media-posts', 'Fast delivery, Canva and Adobe expertise, monthly packages available for businesses.', 5000, 'Karachi', 'PECHS', '', '0311-2223344', 'approved', 9, 0], [3, 9, 'Wooden Dining Table 6 Chairs', 'wooden-dining-table-6-chairs', 'Solid wood dining set, polished finish, excellent for home or office pantry.', 45000, 'Islamabad', 'G-11', '', '0333-9876543', 'approved', 6, 0], ]; foreach ($samples as $row) { $listingStmt->execute($row); } } function categories(): array { static $items = null; if ($items !== null) return $items; $items = db()->query('SELECT * FROM categories ORDER BY id ASC')->fetchAll(); return $items; } function category_map(): array { $map = []; foreach (categories() as $c) { $map[(int)$c['id']] = $c; } return $map; } function find_category_by_slug(string $slug): ?array { $stmt = db()->prepare('SELECT * FROM categories WHERE slug = ? LIMIT 1'); $stmt->execute([$slug]); return $stmt->fetch() ?: null; } function find_listing(int $id): ?array { $stmt = db()->prepare('SELECT l.*, c.name AS category_name, c.slug AS category_slug, u.name AS seller_name, u.email AS seller_email, u.city AS seller_city FROM listings l JOIN categories c ON c.id = l.category_id JOIN users u ON u.id = l.user_id WHERE l.id = ? LIMIT 1'); $stmt->execute([$id]); return $stmt->fetch() ?: null; } function listing_image_url(array $listing): string { if (!empty($listing['image_path']) && is_file(__DIR__ . '/' . ltrim($listing['image_path'], '/'))) { return e($listing['image_path']); } return 'data:image/svg+xml;utf8,' . rawurlencode('Mix'); } function normalize_upload(array $file): ?string { if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) return null; if (($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) { throw new RuntimeException('Image upload failed.'); } if (($file['size'] ?? 0) > MAX_IMAGE_SIZE) { throw new RuntimeException('Image too large. Maximum size is 4 MB.'); } $tmp = $file['tmp_name'] ?? ''; $mime = mime_content_type($tmp) ?: ''; $allowed = [ 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif', ]; if (!isset($allowed[$mime])) { throw new RuntimeException('Only JPG, PNG, WEBP, and GIF images are allowed.'); } $name = date('YmdHis') . '_' . bin2hex(random_bytes(5)) . '.' . $allowed[$mime]; $dest = UPLOAD_DIR . '/' . $name; if (!move_uploaded_file($tmp, $dest)) { throw new RuntimeException('Could not save uploaded image.'); } return 'storage/uploads/' . $name; } function is_favorite(int $listingId): bool { $user = current_user(); if (!$user) return false; $stmt = db()->prepare('SELECT 1 FROM favorites WHERE user_id = ? AND listing_id = ? LIMIT 1'); $stmt->execute([(int)$user['id'], $listingId]); return (bool)$stmt->fetchColumn(); } function count_pending_listings(): int { $stmt = db()->query("SELECT COUNT(*) FROM listings WHERE status = 'pending'"); return (int)$stmt->fetchColumn(); } function search_listings(array $filters = []): array { $where = ["l.status = 'approved'"]; $params = []; if (!empty($filters['q'])) { $where[] = '(l.title LIKE :q OR l.description LIKE :q OR l.city LIKE :q OR l.area LIKE :q)'; $params[':q'] = '%' . $filters['q'] . '%'; } if (!empty($filters['category_id'])) { $where[] = 'l.category_id = :category_id'; $params[':category_id'] = (int)$filters['category_id']; } if (!empty($filters['city'])) { $where[] = 'l.city LIKE :city'; $params[':city'] = $filters['city']; } if (!empty($filters['min_price'])) { $where[] = 'l.price >= :min_price'; $params[':min_price'] = (float)$filters['min_price']; } if (!empty($filters['max_price'])) { $where[] = 'l.price <= :max_price'; $params[':max_price'] = (float)$filters['max_price']; } $sort = $filters['sort'] ?? 'latest'; $orderBy = match ($sort) { 'price_asc' => 'l.price IS NULL ASC, l.price ASC, l.created_at DESC', 'price_desc' => 'l.price DESC, l.created_at DESC', 'popular' => 'l.views DESC, l.created_at DESC', default => 'l.is_featured DESC, l.created_at DESC' }; $sql = 'SELECT l.*, c.name AS category_name, c.slug AS category_slug, u.name AS seller_name FROM listings l JOIN categories c ON c.id = l.category_id JOIN users u ON u.id = l.user_id WHERE ' . implode(' AND ', $where) . ' ORDER BY ' . $orderBy; $stmt = db()->prepare($sql); $stmt->execute($params); return $stmt->fetchAll(); } function render_header(string $title = APP_NAME): void { $user = current_user(); $flashes = get_flashes(); $pending = $user && (int)$user['is_admin'] === 1 ? count_pending_listings() : 0; $currentPage = $_GET['page'] ?? 'home'; ?> <?= e($title) ?> - <?= APP_NAME ?>
prepare('INSERT INTO users (name, email, password_hash, phone, city) VALUES (?, ?, ?, ?, ?)'); $stmt->execute([$name, $email, password_hash($password, PASSWORD_DEFAULT), $phone, $city]); $_SESSION['user_id'] = (int)db()->lastInsertId(); flash('success', 'Welcome to Mix. Your account is ready.'); redirect('?page=dashboard'); } catch (Throwable $e) { flash('error', 'This email is already registered.'); redirect('?page=register'); } } if ($page === 'login' && is_post()) { verify_csrf(); $email = strtolower(trim($_POST['email'] ?? '')); $password = (string)($_POST['password'] ?? ''); $stmt = db()->prepare('SELECT * FROM users WHERE email = ? LIMIT 1'); $stmt->execute([$email]); $user = $stmt->fetch(); if ($user && password_verify($password, $user['password_hash'])) { $_SESSION['user_id'] = (int)$user['id']; flash('success', 'Welcome back, ' . $user['name'] . '.'); redirect('?page=dashboard'); } flash('error', 'Invalid email or password.'); redirect('?page=login'); } if ($page === 'toggle_favorite' && is_post()) { require_login(); verify_csrf(); $user = current_user(); $listingId = (int)($_POST['listing_id'] ?? 0); $back = $_POST['back'] ?? '?page=favorites'; if ($listingId > 0) { if (is_favorite($listingId)) { $stmt = db()->prepare('DELETE FROM favorites WHERE user_id = ? AND listing_id = ?'); $stmt->execute([(int)$user['id'], $listingId]); flash('success', 'Removed from favorites.'); } else { $stmt = db()->prepare('INSERT OR IGNORE INTO favorites (user_id, listing_id) VALUES (?, ?)'); $stmt->execute([(int)$user['id'], $listingId]); flash('success', 'Saved to favorites.'); } } redirect($back ?: '?page=home'); } if ($page === 'create' && is_post()) { require_login(); verify_csrf(); $user = current_user(); $title = trim($_POST['title'] ?? ''); $description = trim($_POST['description'] ?? ''); $categoryId = (int)($_POST['category_id'] ?? 0); $price = trim($_POST['price'] ?? ''); $city = trim($_POST['city'] ?? ''); $area = trim($_POST['area'] ?? ''); $phone = trim($_POST['phone'] ?? $user['phone'] ?? ''); if ($title === '' || $description === '' || $categoryId < 1 || $city === '') { flash('error', 'Please complete title, description, category, and city.'); redirect('?page=create'); } try { $imagePath = !empty($_FILES['image']) ? normalize_upload($_FILES['image']) : null; $slug = slugify($title) . '-' . bin2hex(random_bytes(3)); $status = ((int)$user['is_admin'] === 1) ? 'approved' : 'pending'; $stmt = db()->prepare('INSERT INTO listings (user_id, category_id, title, slug, description, price, city, area, image_path, phone, status, is_featured) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)'); $stmt->execute([ (int)$user['id'], $categoryId, $title, $slug, $description, $price !== '' ? (float)$price : null, $city, $area, $imagePath ?? '', $phone, $status, ]); flash('success', $status === 'approved' ? 'Your ad is live.' : 'Your ad was submitted and is awaiting review.'); redirect('?page=dashboard'); } catch (Throwable $e) { flash('error', $e instanceof RuntimeException ? $e->getMessage() : 'Could not create listing.'); redirect('?page=create'); } } if ($page === 'contact_seller' && is_post()) { verify_csrf(); $listingId = (int)($_POST['listing_id'] ?? 0); $senderName = trim($_POST['sender_name'] ?? ''); $senderEmail = trim($_POST['sender_email'] ?? ''); $senderPhone = trim($_POST['sender_phone'] ?? ''); $message = trim($_POST['message'] ?? ''); if ($listingId < 1 || $senderName === '' || !filter_var($senderEmail, FILTER_VALIDATE_EMAIL) || $message === '') { flash('error', 'Please complete the contact form properly.'); redirect('?page=listing&id=' . $listingId); } $stmt = db()->prepare('INSERT INTO messages (listing_id, sender_name, sender_email, sender_phone, message) VALUES (?, ?, ?, ?, ?)'); $stmt->execute([$listingId, $senderName, $senderEmail, $senderPhone, $message]); flash('success', 'Your message has been recorded for the seller.'); redirect('?page=listing&id=' . $listingId); } if ($page === 'admin_action' && is_post()) { require_admin(); verify_csrf(); $listingId = (int)($_POST['listing_id'] ?? 0); $action = $_POST['action'] ?? ''; if ($listingId > 0) { if ($action === 'approve') { $stmt = db()->prepare("UPDATE listings SET status = 'approved', updated_at = CURRENT_TIMESTAMP WHERE id = ?"); $stmt->execute([$listingId]); flash('success', 'Listing approved.'); } elseif ($action === 'reject') { $stmt = db()->prepare("UPDATE listings SET status = 'rejected', updated_at = CURRENT_TIMESTAMP WHERE id = ?"); $stmt->execute([$listingId]); flash('success', 'Listing rejected.'); } elseif ($action === 'feature') { $stmt = db()->prepare('UPDATE listings SET is_featured = CASE WHEN is_featured = 1 THEN 0 ELSE 1 END, updated_at = CURRENT_TIMESTAMP WHERE id = ?'); $stmt->execute([$listingId]); flash('success', 'Featured status updated.'); } elseif ($action === 'delete') { $stmt = db()->prepare('DELETE FROM listings WHERE id = ?'); $stmt->execute([$listingId]); flash('success', 'Listing deleted.'); } } redirect('?page=admin'); } // ------------------------- // Pages // ------------------------- if ($page === 'home') { render_header('Home'); $featured = db()->query("SELECT l.*, c.name AS category_name, c.slug AS category_slug, u.name AS seller_name FROM listings l JOIN categories c ON c.id = l.category_id JOIN users u ON u.id = l.user_id WHERE l.status = 'approved' ORDER BY l.is_featured DESC, l.created_at DESC LIMIT 8")->fetchAll(); $latest = db()->query("SELECT l.*, c.name AS category_name, c.slug AS category_slug, u.name AS seller_name FROM listings l JOIN categories c ON c.id = l.category_id JOIN users u ON u.id = l.user_id WHERE l.status = 'approved' ORDER BY l.created_at DESC LIMIT 12")->fetchAll(); $totalAds = (int)db()->query("SELECT COUNT(*) FROM listings WHERE status = 'approved'")->fetchColumn(); $totalUsers = (int)db()->query("SELECT COUNT(*) FROM users")->fetchColumn(); ?>
Pakistan classifieds marketplace

Buy, sell and discover everything near you.

Mix keeps the browsing flow familiar and fast: straightforward categories, instant search, clear prices, and direct seller contact.

Browse ads Post your ad
live ads
categories
registered users

Browse by category

Simple paths people already understand.

Featured listings

Highlighted ads with clean, readable cards.
See all ads

Fresh recommendations

Recently published ads from across Pakistan.
trim($_GET['q'] ?? ''), 'category_id' => $category['id'] ?? null, 'city' => trim($_GET['city'] ?? ''), 'min_price' => trim($_GET['min_price'] ?? ''), 'max_price' => trim($_GET['max_price'] ?? ''), 'sort' => $_GET['sort'] ?? 'latest', ]; $results = search_listings($filters); render_header('Browse ads'); ?>

result
Post an ad
No ads matched these filters. Try a wider search or post the first ad in this category.
Listing not found.
'; render_footer(); exit; } $viewer = current_user(); if ($listing['status'] !== 'approved') { if (!$viewer || ((int)$viewer['id'] !== (int)$listing['user_id'] && (int)$viewer['is_admin'] !== 1)) { http_response_code(404); render_header('Not found'); echo '
Listing not found.
'; render_footer(); exit; } } db()->prepare('UPDATE listings SET views = views + 1 WHERE id = ?')->execute([$id]); $listing['views']++; $relatedStmt = db()->prepare("SELECT l.*, c.name AS category_name, c.slug AS category_slug, u.name AS seller_name FROM listings l JOIN categories c ON c.id = l.category_id JOIN users u ON u.id = l.user_id WHERE l.status = 'approved' AND l.category_id = ? AND l.id != ? ORDER BY l.created_at DESC LIMIT 4"); $relatedStmt->execute([(int)$listing['category_id'], $id]); $related = $relatedStmt->fetchAll(); render_header($listing['title']); ?>

Posted

Description

Details

Category
Location
Views
Status

Related ads

More in
No related ads yet.
'; ?>

Create your Mix account

Start buying, selling, and saving ads in one place.

Already have an account?

Login to Mix

Access your account, favorites, and posted ads.

Demo admin: admin@mix.local / admin123

Post your ad

Add a title, choose a category, enter your price, and upload one image.

Cancel
For regular users, new ads go to moderation before they appear publicly.
prepare('SELECT l.*, c.name AS category_name FROM listings l JOIN categories c ON c.id = l.category_id WHERE l.user_id = ? ORDER BY l.created_at DESC'); $stmt->execute([(int)$user['id']]); $items = $stmt->fetchAll(); render_header('Dashboard'); ?>

Welcome,

Manage your account and posted ads.
+ New ad
Email
Phone
City
Role

My listings

ad
You have not posted any ads yet.
Ad Category Price Status Views Published
prepare("SELECT l.*, c.name AS category_name, c.slug AS category_slug, u.name AS seller_name FROM favorites f JOIN listings l ON l.id = f.listing_id JOIN categories c ON c.id = l.category_id JOIN users u ON u.id = l.user_id WHERE f.user_id = ? AND l.status = 'approved' ORDER BY f.created_at DESC"); $stmt->execute([(int)$user['id']]); $items = $stmt->fetchAll(); render_header('Favorites'); ?>

Saved ads

Your favorite listings in one place.
You have not saved any ads yet.
query("SELECT l.*, c.name AS category_name, u.name AS seller_name, u.email AS seller_email FROM listings l JOIN categories c ON c.id = l.category_id JOIN users u ON u.id = l.user_id WHERE l.status IN ('pending', 'approved', 'rejected') ORDER BY CASE WHEN l.status = 'pending' THEN 0 ELSE 1 END, l.created_at DESC")->fetchAll(); render_header('Admin'); ?>

Admin moderation

Review, feature, approve, reject, or delete listings.
No listings to review.
Listing Seller Category Status Featured Actions


'Approve', 'reject' => 'Reject', 'feature' => ((int)$item['is_featured'] ? 'Unfeature' : 'Feature'), 'delete' => 'Delete'] as $act => $label): ?>

Page not found

The page you requested does not exist.

Go to home
No listings found.'; return; } echo '
'; foreach ($items as $item) { $favorite = is_favorite((int)$item['id']); $back = '?page=' . e($_GET['page'] ?? 'home'); if (!empty($_SERVER['QUERY_STRING'])) { $back = '?' . $_SERVER['QUERY_STRING']; } ?>
<?= e($item['title']) ?>
views
'; }