Complete Full-Stack Social Media Application with PHP, MySQL, JavaScript
PROJECT OVERVIEW
A fully functional social media platform with content-based recommendation system that suggests posts, friends, and content based on user interests, interactions, and behavior patterns.
Core Features
User Features
- ✅ User registration and authentication
- ✅ Profile creation and customization
- ✅ Friend requests and connections
- ✅ News feed with personalized content
- ✅ Post creation (text, images, videos)
- ✅ Likes, comments, and shares
- ✅ Real-time notifications
- ✅ Direct messaging
- ✅ Stories (24-hour content)
- ✅ Saved posts
- ✅ Block/Report system
Content-Based Recommendations
- ✅ Personalized news feed algorithm
- ✅ Friend suggestions based on interests
- ✅ Content recommendations based on engagement
- ✅ Trending topics detection
- ✅ Hashtag-based discovery
- ✅ Similar content suggestions
Advanced Features
- ✅ Search with filters
- ✅ Privacy settings
- ✅ Activity logs
- ✅ Analytics dashboard
- ✅ Admin panel
- ✅ Moderation tools
- ✅ Data export
DATABASE SCHEMA (Complete)
CREATE DATABASE social_media_platform;
USE social_media_platform;
-- Users table (core)
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(100),
bio TEXT,
profile_pic VARCHAR(255),
cover_photo VARCHAR(255),
website VARCHAR(255),
location VARCHAR(100),
birth_date DATE,
gender ENUM('male', 'female', 'other', 'prefer_not_to_say'),
phone VARCHAR(20),
account_type ENUM('public', 'private') DEFAULT 'public',
is_verified BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
is_admin BOOLEAN DEFAULT FALSE,
last_login TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_email (email),
INDEX idx_username (username),
INDEX idx_active (is_active),
FULLTEXT INDEX ft_search (username, full_name, bio)
);
-- User preferences for recommendations
CREATE TABLE user_preferences (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
interested_categories JSON,
interested_topics JSON,
preferred_language VARCHAR(10) DEFAULT 'en',
notification_sound BOOLEAN DEFAULT TRUE,
dark_mode BOOLEAN DEFAULT FALSE,
content_filter_level ENUM('low', 'medium', 'high') DEFAULT 'medium',
recommendation_weight FLOAT DEFAULT 1.0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_user (user_id)
);
-- Posts table
CREATE TABLE posts (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
content TEXT,
media_type ENUM('none', 'image', 'video', 'audio', 'document') DEFAULT 'none',
media_url JSON, -- Array of media URLs
thumbnail_url VARCHAR(255),
visibility ENUM('public', 'friends', 'private', 'custom') DEFAULT 'public',
allow_comments BOOLEAN DEFAULT TRUE,
allow_shares BOOLEAN DEFAULT TRUE,
location VARCHAR(255),
tags JSON, -- Array of tagged user IDs
hashtags JSON, -- Array of hashtags
mentions JSON, -- Array of mentioned user IDs
likes_count INT DEFAULT 0,
comments_count INT DEFAULT 0,
shares_count INT DEFAULT 0,
views_count INT DEFAULT 0,
is_pinned BOOLEAN DEFAULT FALSE,
is_sponsored BOOLEAN DEFAULT FALSE,
status ENUM('active', 'deleted', 'reported', 'hidden') DEFAULT 'active',
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,
INDEX idx_user (user_id),
INDEX idx_visibility (visibility),
INDEX idx_created (created_at),
INDEX idx_trending (likes_count, comments_count, shares_count, created_at),
FULLTEXT INDEX ft_content (content)
);
-- Post likes
CREATE TABLE post_likes (
id INT PRIMARY KEY AUTO_INCREMENT,
post_id INT NOT NULL,
user_id INT NOT NULL,
reaction_type ENUM('like', 'love', 'haha', 'wow', 'sad', 'angry') DEFAULT 'like',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_like (post_id, user_id),
INDEX idx_post (post_id)
);
-- Comments
CREATE TABLE comments (
id INT PRIMARY KEY AUTO_INCREMENT,
post_id INT NOT NULL,
user_id INT NOT NULL,
parent_id INT DEFAULT NULL,
content TEXT NOT NULL,
media_url VARCHAR(255),
likes_count INT DEFAULT 0,
is_edited BOOLEAN DEFAULT FALSE,
status ENUM('active', 'deleted', 'hidden') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE,
INDEX idx_post (post_id),
INDEX idx_user (user_id)
);
-- Comment likes
CREATE TABLE comment_likes (
id INT PRIMARY KEY AUTO_INCREMENT,
comment_id INT NOT NULL,
user_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (comment_id) REFERENCES comments(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_like (comment_id, user_id)
);
-- Shares
CREATE TABLE shares (
id INT PRIMARY KEY AUTO_INCREMENT,
post_id INT NOT NULL,
user_id INT NOT NULL,
content TEXT,
visibility ENUM('public', 'friends', 'private') DEFAULT 'public',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_post (post_id)
);
-- Friendships
CREATE TABLE friendships (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
friend_id INT NOT NULL,
status ENUM('pending', 'accepted', 'blocked', 'declined') DEFAULT 'pending',
action_user_id INT, -- User who sent the request
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 (friend_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (action_user_id) REFERENCES users(id) ON DELETE SET NULL,
UNIQUE KEY unique_friendship (user_id, friend_id),
INDEX idx_status (status)
);
-- Follows (for pages/public figures)
CREATE TABLE follows (
id INT PRIMARY KEY AUTO_INCREMENT,
follower_id INT NOT NULL,
following_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (follower_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (following_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_follow (follower_id, following_id),
INDEX idx_follower (follower_id),
INDEX idx_following (following_id)
);
-- Messages
CREATE TABLE messages (
id INT PRIMARY KEY AUTO_INCREMENT,
sender_id INT NOT NULL,
receiver_id INT NOT NULL,
content TEXT NOT NULL,
media_url VARCHAR(255),
is_read BOOLEAN DEFAULT FALSE,
is_delivered BOOLEAN DEFAULT FALSE,
is_deleted BOOLEAN DEFAULT FALSE,
replied_to_id INT DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (receiver_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (replied_to_id) REFERENCES messages(id) ON DELETE SET NULL,
INDEX idx_conversation (sender_id, receiver_id),
INDEX idx_read (is_read)
);
-- Conversations (for grouping messages)
CREATE TABLE conversations (
id INT PRIMARY KEY AUTO_INCREMENT,
participant1_id INT NOT NULL,
participant2_id INT NOT NULL,
last_message_id INT,
last_message_time TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (participant1_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (participant2_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (last_message_id) REFERENCES messages(id) ON DELETE SET NULL,
UNIQUE KEY unique_conversation (participant1_id, participant2_id)
);
-- Notifications
CREATE TABLE notifications (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
actor_id INT, -- User who performed the action
type ENUM('friend_request', 'friend_accept', 'like', 'comment', 'share', 'mention', 'tag', 'message', 'follow', 'birthday') NOT NULL,
reference_id INT, -- ID of related content (post, comment, etc.)
content TEXT,
is_read BOOLEAN DEFAULT FALSE,
is_clicked BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (actor_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_user_read (user_id, is_read),
INDEX idx_created (created_at)
);
-- Stories (temporary content)
CREATE TABLE stories (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
media_url VARCHAR(255) NOT NULL,
media_type ENUM('image', 'video') NOT NULL,
thumbnail_url VARCHAR(255),
caption TEXT,
viewers_count INT DEFAULT 0,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user (user_id),
INDEX idx_expires (expires_at)
);
-- Story views
CREATE TABLE story_views (
id INT PRIMARY KEY AUTO_INCREMENT,
story_id INT NOT NULL,
user_id INT NOT NULL,
viewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (story_id) REFERENCES stories(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_view (story_id, user_id)
);
-- Hashtags
CREATE TABLE hashtags (
id INT PRIMARY KEY AUTO_INCREMENT,
tag VARCHAR(100) UNIQUE NOT NULL,
posts_count INT DEFAULT 0,
last_used TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_tag (tag),
INDEX idx_popular (posts_count)
);
-- Post hashtags relationship
CREATE TABLE post_hashtags (
id INT PRIMARY KEY AUTO_INCREMENT,
post_id INT NOT NULL,
hashtag_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (hashtag_id) REFERENCES hashtags(id) ON DELETE CASCADE,
UNIQUE KEY unique_post_hashtag (post_id, hashtag_id)
);
-- User interests (for recommendations)
CREATE TABLE user_interests (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
interest VARCHAR(100) NOT NULL,
weight FLOAT DEFAULT 1.0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_interest (user_id, interest),
INDEX idx_user (user_id)
);
-- User activity log (for recommendations)
CREATE TABLE user_activities (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
activity_type ENUM('post_view', 'post_like', 'post_comment', 'post_share', 'profile_view', 'search', 'click') NOT NULL,
target_id INT, -- ID of target content
target_type VARCHAR(50), -- 'post', 'user', 'hashtag'
metadata JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_activity (user_id, activity_type),
INDEX idx_created (created_at),
INDEX idx_target (target_id, target_type)
);
-- Content recommendations cache
CREATE TABLE content_recommendations (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
post_id INT NOT NULL,
score DECIMAL(5,4),
reason JSON, -- Reasons for recommendation
is_viewed BOOLEAN DEFAULT FALSE,
is_clicked BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
INDEX idx_user_score (user_id, score),
INDEX idx_expires (expires_at)
);
-- Friend recommendations cache
CREATE TABLE friend_recommendations (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
suggested_user_id INT NOT NULL,
score DECIMAL(5,4),
reason JSON,
is_viewed BOOLEAN DEFAULT FALSE,
is_connected BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (suggested_user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_score (user_id, score)
);
-- Blocks
CREATE TABLE blocks (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
blocked_user_id INT NOT NULL,
reason VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (blocked_user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_block (user_id, blocked_user_id)
);
-- Reports
CREATE TABLE reports (
id INT PRIMARY KEY AUTO_INCREMENT,
reporter_id INT NOT NULL,
reported_type ENUM('user', 'post', 'comment', 'message') NOT NULL,
reported_id INT NOT NULL,
reason VARCHAR(255),
description TEXT,
status ENUM('pending', 'reviewed', 'resolved', 'dismissed') DEFAULT 'pending',
reviewed_by INT,
reviewed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (reporter_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (reviewed_by) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_status (status),
INDEX idx_reported (reported_type, reported_id)
);
-- User sessions
CREATE TABLE user_sessions (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
session_token VARCHAR(255) UNIQUE NOT NULL,
device_info JSON,
ip_address VARCHAR(45),
user_agent TEXT,
last_activity TIMESTAMP,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_token (session_token),
INDEX idx_user (user_id)
);
-- Settings
CREATE TABLE settings (
id INT PRIMARY KEY AUTO_INCREMENT,
setting_key VARCHAR(100) UNIQUE NOT NULL,
setting_value TEXT,
description TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Insert default settings
INSERT INTO settings (setting_key, setting_value, description) VALUES
('site_name', 'SocialConnect', 'Website name'),
('site_description', 'Connect with friends and the world around you', 'Site description'),
('max_upload_size', '100', 'Maximum upload size in MB'),
('allowed_file_types', 'jpg,jpeg,png,gif,mp4,mp3,pdf,doc,docx', 'Allowed file types'),
('posts_per_page', '20', 'Number of posts per page'),
('recommendation_limit', '50', 'Number of recommendations to generate'),
('friend_suggestion_limit', '20', 'Number of friend suggestions'),
('trending_days', '7', 'Days to consider for trending'),
('maintenance_mode', '0', 'Maintenance mode status'),
('registration_enabled', '1', 'Allow new registrations');
BACKEND IMPLEMENTATION
config.php
<?php
session_start();
// Database configuration
define('DB_HOST', 'localhost');
define('DB_NAME', 'social_media_platform');
define('DB_USER', 'root');
define('DB_PASS', '');
// Site configuration
define('SITE_URL', 'http://localhost/social-media');
define('SITE_NAME', 'SocialConnect');
define('UPLOAD_DIR', __DIR__ . '/uploads/');
define('MAX_FILE_SIZE', 100 * 1024 * 1024); // 100MB
define('TIMEZONE', 'UTC');
define('DEBUG_MODE', true);
// Set timezone
date_default_timezone_set(TIMEZONE);
// Error reporting
if (DEBUG_MODE) {
error_reporting(E_ALL);
ini_set('display_errors', 1);
} else {
error_reporting(0);
ini_set('display_errors', 0);
}
// Database connection
try {
$pdo = new PDO(
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
DB_USER,
DB_PASS,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
} catch(PDOException $e) {
die("Connection failed: " . $e->getMessage());
}
// Create upload directories if they don't exist
$upload_dirs = [
UPLOAD_DIR,
UPLOAD_DIR . 'profiles/',
UPLOAD_DIR . 'covers/',
UPLOAD_DIR . 'posts/',
UPLOAD_DIR . 'stories/',
UPLOAD_DIR . 'messages/'
];
foreach ($upload_dirs as $dir) {
if (!file_exists($dir)) {
mkdir($dir, 0777, true);
}
}
// Include helper functions
require_once 'helpers.php';
// Autoload classes
spl_autoload_register(function ($class) {
$file = __DIR__ . '/classes/' . $class . '.php';
if (file_exists($file)) {
require_once $file;
}
});
// Check if user is logged in
function isLoggedIn() {
return isset($_SESSION['user_id']);
}
// Get current user ID
function getCurrentUserId() {
return $_SESSION['user_id'] ?? null;
}
// Get current user data
function getCurrentUser() {
global $pdo;
if (!isLoggedIn()) return null;
static $user = null;
if ($user === null) {
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
}
return $user;
}
// Redirect function
function redirect($url) {
header("Location: " . SITE_URL . $url);
exit;
}
// JSON response function
function jsonResponse($data, $status = 200) {
http_response_code($status);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
?>
helpers.php
<?php
// Helper functions
// Sanitize input
function sanitize($input) {
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}
// Generate random string
function generateRandomString($length = 32) {
return bin2hex(random_bytes($length / 2));
}
// Format date
function formatDate($date, $format = 'M j, Y') {
return date($format, strtotime($date));
}
// Time ago function
function timeAgo($datetime) {
$time = strtotime($datetime);
$now = time();
$diff = $now - $time;
if ($diff < 60) {
return $diff . ' seconds ago';
} elseif ($diff < 3600) {
$mins = floor($diff / 60);
return $mins . ' minute' . ($mins > 1 ? 's' : '') . ' ago';
} elseif ($diff < 86400) {
$hours = floor($diff / 3600);
return $hours . ' hour' . ($hours > 1 ? 's' : '') . ' ago';
} elseif ($diff < 2592000) {
$days = floor($diff / 86400);
return $days . ' day' . ($days > 1 ? 's' : '') . ' ago';
} elseif ($diff < 31536000) {
$months = floor($diff / 2592000);
return $months . ' month' . ($months > 1 ? 's' : '') . ' ago';
} else {
$years = floor($diff / 31536000);
return $years . ' year' . ($years > 1 ? 's' : '') . ' ago';
}
}
// Upload file
function uploadFile($file, $folder, $allowedTypes = ['jpg', 'jpeg', 'png', 'gif', 'mp4', 'mp3']) {
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'message' => 'Upload failed'];
}
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $allowedTypes)) {
return ['success' => false, 'message' => 'File type not allowed'];
}
if ($file['size'] > MAX_FILE_SIZE) {
return ['success' => false, 'message' => 'File too large'];
}
$filename = uniqid() . '_' . time() . '.' . $extension;
$filepath = UPLOAD_DIR . $folder . '/' . $filename;
if (move_uploaded_file($file['tmp_name'], $filepath)) {
return [
'success' => true,
'filename' => $filename,
'path' => 'uploads/' . $folder . '/' . $filename
];
}
return ['success' => false, 'message' => 'Failed to save file'];
}
// Get file URL
function getFileUrl($path) {
if (empty($path)) return null;
return SITE_URL . '/' . $path;
}
// Get profile picture URL
function getProfilePicUrl($user) {
if (!empty($user['profile_pic'])) {
return getFileUrl($user['profile_pic']);
}
return SITE_URL . '/assets/images/default-avatar.png';
}
// Get cover photo URL
function getCoverUrl($user) {
if (!empty($user['cover_photo'])) {
return getFileUrl($user['cover_photo']);
}
return SITE_URL . '/assets/images/default-cover.jpg';
}
// Create notification
function createNotification($userId, $actorId, $type, $referenceId = null, $content = null) {
global $pdo;
$stmt = $pdo->prepare("
INSERT INTO notifications (user_id, actor_id, type, reference_id, content)
VALUES (?, ?, ?, ?, ?)
");
return $stmt->execute([$userId, $actorId, $type, $referenceId, $content]);
}
// Extract hashtags from content
function extractHashtags($content) {
preg_match_all('/#(\w+)/', $content, $matches);
return $matches[1];
}
// Extract mentions from content
function extractMentions($content) {
preg_match_all('/@(\w+)/', $content, $matches);
return $matches[1];
}
// Process hashtags
function processHashtags($postId, $hashtags) {
global $pdo;
foreach ($hashtags as $tag) {
// Insert or update hashtag
$stmt = $pdo->prepare("
INSERT INTO hashtags (tag, posts_count, last_used)
VALUES (?, 1, NOW())
ON DUPLICATE KEY UPDATE
posts_count = posts_count + 1,
last_used = NOW()
");
$stmt->execute([$tag]);
// Get hashtag id
$stmt = $pdo->prepare("SELECT id FROM hashtags WHERE tag = ?");
$stmt->execute([$tag]);
$hashtag = $stmt->fetch();
// Link post to hashtag
$stmt = $pdo->prepare("
INSERT IGNORE INTO post_hashtags (post_id, hashtag_id)
VALUES (?, ?)
");
$stmt->execute([$postId, $hashtag['id']]);
}
}
// Get friendship status
function getFriendshipStatus($userId, $friendId) {
global $pdo;
$stmt = $pdo->prepare("
SELECT status FROM friendships
WHERE (user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)
");
$stmt->execute([$userId, $friendId, $friendId, $userId]);
$result = $stmt->fetch();
if ($result) {
return $result['status'];
}
return 'none';
}
// Check if user is followed
function isFollowing($followerId, $followingId) {
global $pdo;
$stmt = $pdo->prepare("SELECT id FROM follows WHERE follower_id = ? AND following_id = ?");
$stmt->execute([$followerId, $followingId]);
return $stmt->fetch() ? true : false;
}
// Get mutual friends count
function getMutualFriendsCount($userId, $otherUserId) {
global $pdo;
$stmt = $pdo->prepare("
SELECT COUNT(*) as mutual_count
FROM friendships f1
JOIN friendships f2 ON f1.friend_id = f2.friend_id
WHERE f1.user_id = ? AND f1.status = 'accepted'
AND f2.user_id = ? AND f2.status = 'accepted'
AND f1.friend_id != ? AND f2.friend_id != ?
");
$stmt->execute([$userId, $otherUserId, $userId, $otherUserId]);
$result = $stmt->fetch();
return $result['mutual_count'];
}
?>
classes/User.php
<?php
class User {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
// Register new user
public function register($data) {
// Check if user exists
$stmt = $this->pdo->prepare("SELECT id FROM users WHERE username = ? OR email = ?");
$stmt->execute([$data['username'], $data['email']]);
if ($stmt->fetch()) {
return ['success' => false, 'message' => 'Username or email already exists'];
}
// Hash password
$passwordHash = password_hash($data['password'], PASSWORD_DEFAULT);
// Insert user
$stmt = $this->pdo->prepare("
INSERT INTO users (username, email, password_hash, full_name, birth_date, gender)
VALUES (?, ?, ?, ?, ?, ?)
");
if ($stmt->execute([
$data['username'],
$data['email'],
$passwordHash,
$data['full_name'] ?? null,
$data['birth_date'] ?? null,
$data['gender'] ?? null
])) {
$userId = $this->pdo->lastInsertId();
// Create user preferences
$prefStmt = $this->pdo->prepare("
INSERT INTO user_preferences (user_id) VALUES (?)
");
$prefStmt->execute([$userId]);
// Create session
$this->createSession($userId);
return ['success' => true, 'user_id' => $userId];
}
return ['success' => false, 'message' => 'Registration failed'];
}
// Login user
public function login($username, $password) {
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE username = ? OR email = ?");
$stmt->execute([$username, $username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
if (!$user['is_active']) {
return ['success' => false, 'message' => 'Account is deactivated'];
}
// Update last login
$updateStmt = $this->pdo->prepare("UPDATE users SET last_login = NOW() WHERE id = ?");
$updateStmt->execute([$user['id']]);
// Create session
$this->createSession($user['id']);
return ['success' => true, 'user' => $user];
}
return ['success' => false, 'message' => 'Invalid credentials'];
}
// Create user session
private function createSession($userId) {
$_SESSION['user_id'] = $userId;
// Create database session
$token = generateRandomString(64);
$expires = date('Y-m-d H:i:s', strtotime('+30 days'));
$stmt = $this->pdo->prepare("
INSERT INTO user_sessions (user_id, session_token, ip_address, user_agent, expires_at)
VALUES (?, ?, ?, ?, ?)
");
$stmt->execute([
$userId,
$token,
$_SERVER['REMOTE_ADDR'] ?? null,
$_SERVER['HTTP_USER_AGENT'] ?? null,
$expires
]);
setcookie('session_token', $token, strtotime('+30 days'), '/', '', true, true);
}
// Get user by ID
public function getById($userId) {
$stmt = $this->pdo->prepare("
SELECT u.*,
(SELECT COUNT(*) FROM friendships WHERE (user_id = u.id OR friend_id = u.id) AND status = 'accepted') as friends_count,
(SELECT COUNT(*) FROM posts WHERE user_id = u.id AND status = 'active') as posts_count,
(SELECT COUNT(*) FROM follows WHERE following_id = u.id) as followers_count,
(SELECT COUNT(*) FROM follows WHERE follower_id = u.id) as following_count
FROM users u
WHERE u.id = ? AND u.is_active = 1
");
$stmt->execute([$userId]);
return $stmt->fetch();
}
// Get user by username
public function getByUsername($username) {
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE username = ? AND is_active = 1");
$stmt->execute([$username]);
return $stmt->fetch();
}
// Update profile
public function updateProfile($userId, $data) {
$allowed = ['full_name', 'bio', 'website', 'location', 'phone', 'account_type'];
$updates = [];
$params = [];
foreach ($data as $key => $value) {
if (in_array($key, $allowed)) {
$updates[] = "$key = ?";
$params[] = $value;
}
}
if (empty($updates)) {
return false;
}
$params[] = $userId;
$sql = "UPDATE users SET " . implode(', ', $updates) . " WHERE id = ?";
$stmt = $this->pdo->prepare($sql);
return $stmt->execute($params);
}
// Update profile picture
public function updateProfilePic($userId, $file) {
$result = uploadFile($file, 'profiles', ['jpg', 'jpeg', 'png', 'gif']);
if ($result['success']) {
$stmt = $this->pdo->prepare("UPDATE users SET profile_pic = ? WHERE id = ?");
return $stmt->execute([$result['path'], $userId]);
}
return false;
}
// Update cover photo
public function updateCoverPhoto($userId, $file) {
$result = uploadFile($file, 'covers', ['jpg', 'jpeg', 'png', 'gif']);
if ($result['success']) {
$stmt = $this->pdo->prepare("UPDATE users SET cover_photo = ? WHERE id = ?");
return $stmt->execute([$result['path'], $userId]);
}
return false;
}
// Search users
public function search($query, $limit = 20, $offset = 0) {
$stmt = $this->pdo->prepare("
SELECT id, username, full_name, profile_pic, bio
FROM users
WHERE (username LIKE ? OR full_name LIKE ? OR bio LIKE ?)
AND is_active = 1
ORDER BY
CASE
WHEN username = ? THEN 1
WHEN full_name = ? THEN 2
WHEN username LIKE ? THEN 3
WHEN full_name LIKE ? THEN 4
ELSE 5
END
LIMIT ? OFFSET ?
");
$searchTerm = "%$query%";
$exactTerm = $query;
$startTerm = "$query%";
$stmt->execute([
$searchTerm, $searchTerm, $searchTerm,
$exactTerm, $exactTerm,
$startTerm, $startTerm,
$limit, $offset
]);
return $stmt->fetchAll();
}
// Get followers
public function getFollowers($userId, $limit = 20) {
$stmt = $this->pdo->prepare("
SELECT u.*
FROM follows f
JOIN users u ON f.follower_id = u.id
WHERE f.following_id = ? AND u.is_active = 1
ORDER BY f.created_at DESC
LIMIT ?
");
$stmt->execute([$userId, $limit]);
return $stmt->fetchAll();
}
// Get following
public function getFollowing($userId, $limit = 20) {
$stmt = $this->pdo->prepare("
SELECT u.*
FROM follows f
JOIN users u ON f.following_id = u.id
WHERE f.follower_id = ? AND u.is_active = 1
ORDER BY f.created_at DESC
LIMIT ?
");
$stmt->execute([$userId, $limit]);
return $stmt->fetchAll();
}
// Get friends
public function getFriends($userId, $limit = 20) {
$stmt = $this->pdo->prepare("
SELECT u.*,
(SELECT COUNT(*) FROM messages WHERE (sender_id = ? AND receiver_id = u.id) OR (sender_id = u.id AND receiver_id = ?)) as messages_count
FROM friendships f
JOIN users u ON (f.user_id = u.id OR f.friend_id = u.id)
WHERE (f.user_id = ? OR f.friend_id = ?)
AND f.status = 'accepted'
AND u.id != ?
AND u.is_active = 1
ORDER BY messages_count DESC
LIMIT ?
");
$stmt->execute([$userId, $userId, $userId, $userId, $userId, $limit]);
return $stmt->fetchAll();
}
}
?>
classes/Post.php
<?php
class Post {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
// Create new post
public function create($userId, $data, $files = null) {
// Handle media upload
$mediaUrls = [];
$mediaType = 'none';
if ($files && isset($files['media'])) {
foreach ($files['media']['tmp_name'] as $key => $tmp_name) {
if ($files['media']['error'][$key] == 0) {
$file = [
'name' => $files['media']['name'][$key],
'type' => $files['media']['type'][$key],
'tmp_name' => $tmp_name,
'error' => $files['media']['error'][$key],
'size' => $files['media']['size'][$key]
];
$result = uploadFile($file, 'posts', ['jpg', 'jpeg', 'png', 'gif', 'mp4', 'mp3']);
if ($result['success']) {
$mediaUrls[] = $result['path'];
$mediaType = $this->getMediaType($file['type']);
}
}
}
}
// Process hashtags
$hashtags = extractHashtags($data['content']);
// Process mentions
$mentions = extractMentions($data['content']);
$mentionedUsers = $this->getUserIdsFromUsernames($mentions);
// Insert post
$stmt = $this->pdo->prepare("
INSERT INTO posts (user_id, content, media_type, media_url, visibility, allow_comments, allow_shares, location, hashtags, mentions)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$success = $stmt->execute([
$userId,
$data['content'],
$mediaType,
json_encode($mediaUrls),
$data['visibility'] ?? 'public',
$data['allow_comments'] ?? 1,
$data['allow_shares'] ?? 1,
$data['location'] ?? null,
json_encode($hashtags),
json_encode($mentionedUsers)
]);
if ($success) {
$postId = $this->pdo->lastInsertId();
// Process hashtags
if (!empty($hashtags)) {
processHashtags($postId, $hashtags);
}
// Create notifications for mentions
foreach ($mentionedUsers as $mentionedUserId) {
if ($mentionedUserId != $userId) {
createNotification($mentionedUserId, $userId, 'mention', $postId);
}
}
return ['success' => true, 'post_id' => $postId];
}
return ['success' => false, 'message' => 'Failed to create post'];
}
// Get post by ID
public function getById($postId, $userId = null) {
$stmt = $this->pdo->prepare("
SELECT p.*,
u.username, u.full_name, u.profile_pic,
(SELECT COUNT(*) FROM post_likes WHERE post_id = p.id) as likes_count,
(SELECT COUNT(*) FROM comments WHERE post_id = p.id AND status = 'active') as comments_count,
(SELECT COUNT(*) FROM shares WHERE post_id = p.id) as shares_count,
? IN (SELECT user_id FROM post_likes WHERE post_id = p.id) as user_liked,
? IN (SELECT user_id FROM post_likes WHERE post_id = p.id AND reaction_type = 'like') as user_liked_type,
(SELECT reaction_type FROM post_likes WHERE post_id = p.id AND user_id = ?) as user_reaction
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.id = ? AND p.status = 'active'
");
$stmt->execute([$userId, $userId, $userId, $postId]);
$post = $stmt->fetch();
if ($post) {
$post['media_url'] = json_decode($post['media_url'], true);
$post['hashtags'] = json_decode($post['hashtags'], true);
$post['mentions'] = json_decode($post['mentions'], true);
// Check privacy
if (!$this->canViewPost($post, $userId)) {
return null;
}
// Increment views
$this->incrementViews($postId);
}
return $post;
}
// Check if user can view post
private function canViewPost($post, $userId) {
if ($post['visibility'] == 'public') {
return true;
}
if ($post['visibility'] == 'friends') {
$status = getFriendshipStatus($userId, $post['user_id']);
return $status == 'accepted';
}
if ($post['visibility'] == 'private') {
return $userId == $post['user_id'];
}
return false;
}
// Get feed posts
public function getFeed($userId, $limit = 20, $offset = 0) {
// Get friends and following
$friends = $this->getFriendIds($userId);
$following = $this->getFollowingIds($userId);
$userIds = array_merge([$userId], $friends, $following);
$placeholders = implode(',', array_fill(0, count($userIds), '?'));
$stmt = $this->pdo->prepare("
SELECT p.*,
u.username, u.full_name, u.profile_pic,
(SELECT COUNT(*) FROM post_likes WHERE post_id = p.id) as likes_count,
(SELECT COUNT(*) FROM comments WHERE post_id = p.id AND status = 'active') as comments_count,
? IN (SELECT user_id FROM post_likes WHERE post_id = p.id) as user_liked
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.user_id IN ($placeholders)
AND p.status = 'active'
AND p.visibility IN ('public', 'friends')
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?
");
$params = array_merge([$userId], $userIds, [$limit, $offset]);
$stmt->execute($params);
return $stmt->fetchAll();
}
// Like/Unlike post
public function like($postId, $userId, $reaction = 'like') {
// Check if already liked
$stmt = $this->pdo->prepare("SELECT id, reaction_type FROM post_likes WHERE post_id = ? AND user_id = ?");
$stmt->execute([$postId, $userId]);
$existing = $stmt->fetch();
if ($existing) {
if ($existing['reaction_type'] == $reaction) {
// Remove like
$stmt = $this->pdo->prepare("DELETE FROM post_likes WHERE post_id = ? AND user_id = ?");
$stmt->execute([$postId, $userId]);
$this->updateLikeCount($postId);
return ['action' => 'removed'];
} else {
// Update reaction
$stmt = $this->pdo->prepare("UPDATE post_likes SET reaction_type = ? WHERE post_id = ? AND user_id = ?");
$stmt->execute([$reaction, $postId, $userId]);
$this->updateLikeCount($postId);
return ['action' => 'updated', 'reaction' => $reaction];
}
} else {
// Add like
$stmt = $this->pdo->prepare("INSERT INTO post_likes (post_id, user_id, reaction_type) VALUES (?, ?, ?)");
$stmt->execute([$postId, $userId, $reaction]);
$this->updateLikeCount($postId);
// Create notification
$post = $this->getById($postId);
if ($post && $post['user_id'] != $userId) {
createNotification($post['user_id'], $userId, 'like', $postId);
}
return ['action' => 'added', 'reaction' => $reaction];
}
}
// Update like count
private function updateLikeCount($postId) {
$stmt = $this->pdo->prepare("
UPDATE posts SET likes_count = (SELECT COUNT(*) FROM post_likes WHERE post_id = ?)
WHERE id = ?
");
$stmt->execute([$postId, $postId]);
}
// Increment views
private function incrementViews($postId) {
$stmt = $this->pdo->prepare("UPDATE posts SET views_count = views_count + 1 WHERE id = ?");
$stmt->execute([$postId]);
}
// Add comment
public function addComment($postId, $userId, $content, $parentId = null) {
$stmt = $this->pdo->prepare("
INSERT INTO comments (post_id, user_id, parent_id, content)
VALUES (?, ?, ?, ?)
");
if ($stmt->execute([$postId, $userId, $parentId, $content])) {
$commentId = $this->pdo->lastInsertId();
// Update comment count
$this->updateCommentCount($postId);
// Create notification
$post = $this->getById($postId);
if ($post && $post['user_id'] != $userId) {
createNotification($post['user_id'], $userId, 'comment', $postId);
}
return $this->getComment($commentId);
}
return null;
}
// Get comments for post
public function getComments($postId) {
$stmt = $this->pdo->prepare("
SELECT c.*,
u.username, u.full_name, u.profile_pic,
(SELECT COUNT(*) FROM comment_likes WHERE comment_id = c.id) as likes_count
FROM comments c
JOIN users u ON c.user_id = u.id
WHERE c.post_id = ? AND c.parent_id IS NULL AND c.status = 'active'
ORDER BY c.created_at DESC
");
$stmt->execute([$postId]);
$comments = $stmt->fetchAll();
foreach ($comments as &$comment) {
$comment['replies'] = $this->getCommentReplies($comment['id']);
}
return $comments;
}
// Get comment replies
private function getCommentReplies($commentId) {
$stmt = $this->pdo->prepare("
SELECT c.*,
u.username, u.full_name, u.profile_pic,
(SELECT COUNT(*) FROM comment_likes WHERE comment_id = c.id) as likes_count
FROM comments c
JOIN users u ON c.user_id = u.id
WHERE c.parent_id = ? AND c.status = 'active'
ORDER BY c.created_at ASC
");
$stmt->execute([$commentId]);
return $stmt->fetchAll();
}
// Get single comment
private function getComment($commentId) {
$stmt = $this->pdo->prepare("
SELECT c.*,
u.username, u.full_name, u.profile_pic,
(SELECT COUNT(*) FROM comment_likes WHERE comment_id = c.id) as likes_count
FROM comments c
JOIN users u ON c.user_id = u.id
WHERE c.id = ?
");
$stmt->execute([$commentId]);
return $stmt->fetch();
}
// Update comment count
private function updateCommentCount($postId) {
$stmt = $this->pdo->prepare("
UPDATE posts SET comments_count = (SELECT COUNT(*) FROM comments WHERE post_id = ? AND status = 'active')
WHERE id = ?
");
$stmt->execute([$postId, $postId]);
}
// Share post
public function share($postId, $userId, $content = null, $visibility = 'public') {
$stmt = $this->pdo->prepare("
INSERT INTO shares (post_id, user_id, content, visibility)
VALUES (?, ?, ?, ?)
");
if ($stmt->execute([$postId, $userId, $content, $visibility])) {
// Update share count
$this->updateShareCount($postId);
// Create notification
$post = $this->getById($postId);
if ($post && $post['user_id'] != $userId) {
createNotification($post['user_id'], $userId, 'share', $postId);
}
return true;
}
return false;
}
// Update share count
private function updateShareCount($postId) {
$stmt = $this->pdo->prepare("
UPDATE posts SET shares_count = (SELECT COUNT(*) FROM shares WHERE post_id = ?)
WHERE id = ?
");
$stmt->execute([$postId, $postId]);
}
// Get friend IDs
private function getFriendIds($userId) {
$stmt = $this->pdo->prepare("
SELECT CASE
WHEN user_id = ? THEN friend_id
ELSE user_id
END as friend_id
FROM friendships
WHERE (user_id = ? OR friend_id = ?) AND status = 'accepted'
");
$stmt->execute([$userId, $userId, $userId]);
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
// Get following IDs
private function getFollowingIds($userId) {
$stmt = $this->pdo->prepare("
SELECT following_id FROM follows WHERE follower_id = ?
");
$stmt->execute([$userId]);
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
// Get user IDs from usernames
private function getUserIdsFromUsernames($usernames) {
if (empty($usernames)) {
return [];
}
$placeholders = implode(',', array_fill(0, count($usernames), '?'));
$stmt = $this->pdo->prepare("SELECT id FROM users WHERE username IN ($placeholders)");
$stmt->execute($usernames);
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
// Get media type from mime
private function getMediaType($mime) {
if (strpos($mime, 'image') !== false) return 'image';
if (strpos($mime, 'video') !== false) return 'video';
if (strpos($mime, 'audio') !== false) return 'audio';
return 'document';
}
}
?>
classes/SocialRecommender.php (Core Recommendation Algorithm)
<?php
class SocialRecommender {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
// Main recommendation engine
public function getRecommendations($userId, $type = 'feed', $limit = 20) {
switch ($type) {
case 'feed':
return $this->getFeedRecommendations($userId, $limit);
case 'friends':
return $this->getFriendRecommendations($userId, $limit);
case 'content':
return $this->getContentRecommendations($userId, $limit);
case 'trending':
return $this->getTrendingContent($limit);
default:
return [];
}
}
// Feed recommendations (personalized posts)
private function getFeedRecommendations($userId, $limit) {
// Check cache
$cached = $this->getCachedRecommendations($userId, 'feed', $limit);
if ($cached) {
return $cached;
}
// Get user interests and behavior
$userProfile = $this->buildUserProfile($userId);
// Get candidate posts
$candidates = $this->getCandidatePosts($userId);
// Score each post
$recommendations = [];
foreach ($candidates as $post) {
$score = $this->calculatePostScore($post, $userProfile);
if ($score > 0.3) {
$recommendations[] = [
'post' => $post,
'score' => $score,
'reasons' => $this->getRecommendationReasons($post, $userProfile)
];
}
}
// Sort by score
usort($recommendations, function($a, $b) {
return $b['score'] <=> $a['score'];
});
$recommendations = array_slice($recommendations, 0, $limit);
// Cache results
$this->cacheRecommendations($userId, 'feed', $recommendations);
return $recommendations;
}
// Friend recommendations
private function getFriendRecommendations($userId, $limit) {
// Check cache
$cached = $this->getCachedRecommendations($userId, 'friends', $limit);
if ($cached) {
return $cached;
}
// Get existing friends
$existingFriends = $this->getExistingFriendIds($userId);
// Get mutual friends and interests
$candidates = $this->getFriendCandidates($userId, $existingFriends);
// Score each candidate
$recommendations = [];
foreach ($candidates as $candidate) {
$score = $this->calculateFriendScore($userId, $candidate);
if ($score > 0.4) {
$recommendations[] = [
'user' => $candidate,
'score' => $score,
'reasons' => $this->getFriendReasons($userId, $candidate)
];
}
}
// Sort by score
usort($recommendations, function($a, $b) {
return $b['score'] <=> $a['score'];
});
$recommendations = array_slice($recommendations, 0, $limit);
// Cache results
$this->cacheRecommendations($userId, 'friends', $recommendations);
return $recommendations;
}
// Build user profile from activities
private function buildUserProfile($userId) {
$profile = [
'interests' => [],
'categories' => [],
'hashtags' => [],
'active_hours' => [],
'engagement_score' => 0,
'content_preferences' => []
];
// Get user interests from profile
$stmt = $this->pdo->prepare("SELECT interested_topics FROM user_preferences WHERE user_id = ?");
$stmt->execute([$userId]);
$prefs = $stmt->fetch();
if ($prefs && $prefs['interested_topics']) {
$profile['interests'] = json_decode($prefs['interested_topics'], true) ?? [];
}
// Analyze user activities (last 30 days)
$stmt = $this->pdo->prepare("
SELECT activity_type, target_type, metadata,
COUNT(*) as activity_count,
HOUR(created_at) as hour
FROM user_activities
WHERE user_id = ? AND created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY activity_type, target_type, HOUR(created_at)
ORDER BY activity_count DESC
");
$stmt->execute([$userId]);
$activities = $stmt->fetchAll();
foreach ($activities as $activity) {
// Track active hours
$profile['active_hours'][$activity['hour']] =
($profile['active_hours'][$activity['hour']] ?? 0) + $activity['activity_count'];
// Track content preferences
if ($activity['target_type'] == 'post') {
$metadata = json_decode($activity['metadata'], true);
if (isset($metadata['category'])) {
$profile['categories'][$metadata['category']] =
($profile['categories'][$metadata['category']] ?? 0) + $activity['activity_count'];
}
if (isset($metadata['hashtags'])) {
foreach ($metadata['hashtags'] as $tag) {
$profile['hashtags'][$tag] =
($profile['hashtags'][$tag] ?? 0) + $activity['activity_count'];
}
}
}
}
// Calculate engagement score
$totalActivities = array_sum($activities);
$profile['engagement_score'] = min(1, $totalActivities / 100);
return $profile;
}
// Calculate post relevance score
private function calculatePostScore($post, $userProfile) {
$weights = [
'interest_match' => 0.35,
'hashtag_match' => 0.25,
'recency' => 0.15,
'popularity' => 0.15,
'engagement' => 0.10
];
$scores = [
'interest_match' => $this->calculateInterestMatch($post, $userProfile),
'hashtag_match' => $this->calculateHashtagMatch($post, $userProfile),
'recency' => $this->calculateRecencyScore($post['created_at']),
'popularity' => $this->calculatePopularityScore($post),
'engagement' => $this->calculateEngagementScore($post, $userProfile)
];
$total = 0;
foreach ($weights as $factor => $weight) {
$total += $scores[$factor] * $weight;
}
return $total;
}
// Interest match score
private function calculateInterestMatch($post, $userProfile) {
if (empty($userProfile['interests'])) {
return 0.5;
}
$postContent = strtolower($post['content'] ?? '');
$matchCount = 0;
foreach ($userProfile['interests'] as $interest) {
if (strpos($postContent, strtolower($interest)) !== false) {
$matchCount++;
}
}
return min(1, $matchCount / count($userProfile['interests']));
}
// Hashtag match score
private function calculateHashtagMatch($post, $userProfile) {
$postHashtags = json_decode($post['hashtags'], true) ?? [];
if (empty($postHashtags) || empty($userProfile['hashtags'])) {
return 0;
}
$matchCount = 0;
foreach ($postHashtags as $tag) {
if (isset($userProfile['hashtags'][$tag])) {
$matchCount += $userProfile['hashtags'][$tag];
}
}
return min(1, $matchCount / count($postHashtags));
}
// Recency score (newer posts get higher scores)
private function calculateRecencyScore($createdAt) {
$hours = (time() - strtotime($createdAt)) / 3600;
return max(0, 1 - ($hours / 72)); // Decay over 3 days
}
// Popularity score
private function calculatePopularityScore($post) {
$likes = $post['likes_count'] ?? 0;
$comments = $post['comments_count'] ?? 0;
$shares = $post['shares_count'] ?? 0;
$score = ($likes * 1 + $comments * 2 + $shares * 3) / 100;
return min(1, $score);
}
// Engagement score (based on user's past behavior)
private function calculateEngagementScore($post, $userProfile) {
// Check if user has engaged with similar content
return $userProfile['engagement_score'] ?? 0.5;
}
// Calculate friend recommendation score
private function calculateFriendScore($userId, $candidate) {
$weights = [
'mutual_friends' => 0.4,
'common_interests' => 0.3,
'interaction_score' => 0.2,
'location_score' => 0.1
];
$scores = [
'mutual_friends' => $this->calculateMutualFriendsScore($userId, $candidate['id']),
'common_interests' => $this->calculateCommonInterestsScore($userId, $candidate['id']),
'interaction_score' => $this->calculateInteractionScore($userId, $candidate['id']),
'location_score' => $this->calculateLocationScore($userId, $candidate)
];
$total = 0;
foreach ($weights as $factor => $weight) {
$total += $scores[$factor] * $weight;
}
return $total;
}
// Mutual friends score
private function calculateMutualFriendsScore($userId, $candidateId) {
$mutualCount = getMutualFriendsCount($userId, $candidateId);
return min(1, $mutualCount / 10); // Cap at 10 mutual friends
}
// Common interests score
private function calculateCommonInterestsScore($userId, $candidateId) {
$stmt = $this->pdo->prepare("
SELECT interest FROM user_interests WHERE user_id = ?
");
$stmt->execute([$userId]);
$userInterests = $stmt->fetchAll(PDO::FETCH_COLUMN);
$stmt->execute([$candidateId]);
$candidateInterests = $stmt->fetchAll(PDO::FETCH_COLUMN);
$common = array_intersect($userInterests, $candidateInterests);
return count($common) / max(1, count($userInterests));
}
// Interaction score (how often they interact)
private function calculateInteractionScore($userId, $candidateId) {
$stmt = $this->pdo->prepare("
SELECT COUNT(*) as interactions
FROM user_activities
WHERE user_id = ? AND target_id = ? AND target_type = 'user'
OR user_id = ? AND target_id = ? AND target_type = 'user'
");
$stmt->execute([$userId, $candidateId, $candidateId, $userId]);
$result = $stmt->fetch();
return min(1, ($result['interactions'] ?? 0) / 20);
}
// Location score
private function calculateLocationScore($userId, $candidate) {
// Get user location
$stmt = $this->pdo->prepare("SELECT location FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch();
if (empty($user['location']) || empty($candidate['location'])) {
return 0.5;
}
return $user['location'] == $candidate['location'] ? 1 : 0.3;
}
// Get friend candidates
private function getFriendCandidates($userId, $existingFriends) {
$excludeIds = array_merge([$userId], $existingFriends);
$placeholders = implode(',', array_fill(0, count($excludeIds), '?'));
$stmt = $this->pdo->prepare("
SELECT u.*
FROM users u
WHERE u.id NOT IN ($placeholders)
AND u.is_active = 1
ORDER BY RAND()
LIMIT 100
");
$stmt->execute($excludeIds);
return $stmt->fetchAll();
}
// Get candidate posts for feed
private function getCandidatePosts($userId) {
// Get friends and followed users
$friendIds = $this->getFriendIds($userId);
$followingIds = $this->getFollowingIds($userId);
$userIds = array_merge($friendIds, $followingIds);
if (empty($userIds)) {
return [];
}
$placeholders = implode(',', array_fill(0, count($userIds), '?'));
$stmt = $this->pdo->prepare("
SELECT p.*, u.username, u.full_name, u.profile_pic
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.user_id IN ($placeholders)
AND p.status = 'active'
AND p.visibility IN ('public', 'friends')
AND p.created_at > DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY p.created_at DESC
LIMIT 200
");
$stmt->execute($userIds);
return $stmt->fetchAll();
}
// Get trending content
private function getTrendingContent($limit) {
$stmt = $this->pdo->prepare("
SELECT p.*, u.username, u.full_name, u.profile_pic,
(p.likes_count * 1 + p.comments_count * 2 + p.shares_count * 3 + p.views_count * 0.1) as trend_score
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.status = 'active'
AND p.visibility = 'public'
AND p.created_at > DATE_SUB(NOW(), INTERVAL 3 DAY)
ORDER BY trend_score DESC
LIMIT ?
");
$stmt->execute([$limit]);
return $stmt->fetchAll();
}
// Get trending hashtags
public function getTrendingHashtags($limit = 10) {
$stmt = $this->pdo->prepare("
SELECT tag, posts_count, last_used
FROM hashtags
WHERE last_used > DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY posts_count DESC, last_used DESC
LIMIT ?
");
$stmt->execute([$limit]);
return $stmt->fetchAll();
}
// Get recommendation reasons
private function getRecommendationReasons($post, $userProfile) {
$reasons = [];
if ($this->calculateInterestMatch($post, $userProfile) > 0.5) {
$reasons[] = 'Based on your interests';
}
if ($this->calculateHashtagMatch($post, $userProfile) > 0.3) {
$reasons[] = 'You follow similar hashtags';
}
if ($this->calculatePopularityScore($post) > 0.7) {
$reasons[] = 'Trending in your network';
}
return $reasons;
}
// Get friend recommendation reasons
private function getFriendReasons($userId, $candidate) {
$reasons = [];
$mutualCount = getMutualFriendsCount($userId, $candidate['id']);
if ($mutualCount > 0) {
$reasons[] = "$mutualCount mutual friends";
}
if ($this->calculateCommonInterestsScore($userId, $candidate['id']) > 0.5) {
$reasons[] = 'Similar interests';
}
return $reasons;
}
// Cache recommendations
private function cacheRecommendations($userId, $type, $recommendations) {
$table = $type == 'feed' ? 'content_recommendations' : 'friend_recommendations';
$idField = $type == 'feed' ? 'post_id' : 'suggested_user_id';
// Clear old cache
$stmt = $this->pdo->prepare("DELETE FROM $table WHERE user_id = ?");
$stmt->execute([$userId]);
// Insert new recommendations
$stmt = $this->pdo->prepare("
INSERT INTO $table (user_id, $idField, score, reason, expires_at)
VALUES (?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL 1 DAY))
");
foreach ($recommendations as $rec) {
$id = $type == 'feed' ? $rec['post']['id'] : $rec['user']['id'];
$stmt->execute([
$userId,
$id,
$rec['score'],
json_encode($rec['reasons'] ?? [])
]);
}
}
// Get cached recommendations
private function getCachedRecommendations($userId, $type, $limit) {
$table = $type == 'feed' ? 'content_recommendations' : 'friend_recommendations';
$idField = $type == 'feed' ? 'post_id' : 'suggested_user_id';
$stmt = $this->pdo->prepare("
SELECT * FROM $table
WHERE user_id = ? AND expires_at > NOW()
ORDER BY score DESC
LIMIT ?
");
$stmt->execute([$userId, $limit]);
return $stmt->fetchAll();
}
// Helper: Get friend IDs
private function getFriendIds($userId) {
$stmt = $this->pdo->prepare("
SELECT CASE
WHEN user_id = ? THEN friend_id
ELSE user_id
END as friend_id
FROM friendships
WHERE (user_id = ? OR friend_id = ?) AND status = 'accepted'
");
$stmt->execute([$userId, $userId, $userId]);
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
// Helper: Get following IDs
private function getFollowingIds($userId) {
$stmt = $this->pdo->prepare("SELECT following_id FROM follows WHERE follower_id = ?");
$stmt->execute([$userId]);
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
// Helper: Get existing friend IDs
private function getExistingFriendIds($userId) {
$stmt = $this->pdo->prepare("
SELECT friend_id FROM friendships WHERE user_id = ? AND status = 'accepted'
UNION
SELECT user_id FROM friendships WHERE friend_id = ? AND status = 'accepted'
");
$stmt->execute([$userId, $userId]);
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
}
?>
classes/Message.php
<?php
class Message {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
// Send message
public function send($senderId, $receiverId, $content, $file = null) {
$mediaUrl = null;
if ($file) {
$result = uploadFile($file, 'messages', ['jpg', 'jpeg', 'png', 'gif', 'mp4', 'mp3', 'pdf', 'doc', 'docx']);
if ($result['success']) {
$mediaUrl = $result['path'];
}
}
// Insert message
$stmt = $this->pdo->prepare("
INSERT INTO messages (sender_id, receiver_id, content, media_url)
VALUES (?, ?, ?, ?)
");
if ($stmt->execute([$senderId, $receiverId, $content, $mediaUrl])) {
$messageId = $this->pdo->lastInsertId();
// Update or create conversation
$this->updateConversation($senderId, $receiverId, $messageId);
// Create notification
createNotification($receiverId, $senderId, 'message', $messageId);
return $this->getMessage($messageId);
}
return null;
}
// Get conversation between two users
public function getConversation($userId1, $userId2, $limit = 50, $before = null) {
$query = "
SELECT m.*,
u_sender.username as sender_username,
u_sender.full_name as sender_name,
u_sender.profile_pic as sender_pic
FROM messages m
JOIN users u_sender ON m.sender_id = u_sender.id
WHERE (m.sender_id = ? AND m.receiver_id = ?)
OR (m.sender_id = ? AND m.receiver_id = ?)
";
$params = [$userId1, $userId2, $userId2, $userId1];
if ($before) {
$query .= " AND m.id < ?";
$params[] = $before;
}
$query .= " ORDER BY m.created_at DESC LIMIT ?";
$params[] = $limit;
$stmt = $this->pdo->prepare($query);
$stmt->execute($params);
$messages = $stmt->fetchAll();
// Mark messages as read
$this->markAsRead($userId2, $userId1);
return array_reverse($messages);
}
// Get all conversations for user
public function getConversations($userId) {
$stmt = $this->pdo->prepare("
SELECT c.*,
u.id as other_user_id,
u.username,
u.full_name,
u.profile_pic,
m.content as last_message,
m.created_at as last_message_time,
m.sender_id as last_message_sender,
(SELECT COUNT(*) FROM messages WHERE receiver_id = ? AND sender_id = u.id AND is_read = 0) as unread_count
FROM conversations c
JOIN users u ON (c.participant1_id = u.id OR c.participant2_id = u.id)
LEFT JOIN messages m ON c.last_message_id = m.id
WHERE (c.participant1_id = ? OR c.participant2_id = ?)
AND u.id != ?
ORDER BY c.last_message_time DESC
");
$stmt->execute([$userId, $userId, $userId, $userId]);
return $stmt->fetchAll();
}
// Get single message
private function getMessage($messageId) {
$stmt = $this->pdo->prepare("
SELECT m.*,
u_sender.username as sender_username,
u_sender.full_name as sender_name,
u_sender.profile_pic as sender_pic
FROM messages m
JOIN users u_sender ON m.sender_id = u_sender.id
WHERE m.id = ?
");
$stmt->execute([$messageId]);
return $stmt->fetch();
}
// Update conversation
private function updateConversation($user1, $user2, $lastMessageId) {
// Check if conversation exists
$stmt = $this->pdo->prepare("
SELECT id FROM conversations
WHERE (participant1_id = ? AND participant2_id = ?)
OR (participant1_id = ? AND participant2_id = ?)
");
$stmt->execute([$user1, $user2, $user2, $user1]);
$conversation = $stmt->fetch();
if ($conversation) {
// Update existing
$stmt = $this->pdo->prepare("
UPDATE conversations
SET last_message_id = ?, last_message_time = NOW()
WHERE id = ?
");
$stmt->execute([$lastMessageId, $conversation['id']]);
} else {
// Create new
$stmt = $this->pdo->prepare("
INSERT INTO conversations (participant1_id, participant2_id, last_message_id, last_message_time)
VALUES (?, ?, ?, NOW())
");
$stmt->execute([$user1, $user2, $lastMessageId]);
}
}
// Mark messages as read
public function markAsRead($userId, $otherUserId) {
$stmt = $this->pdo->prepare("
UPDATE messages
SET is_read = 1
WHERE receiver_id = ? AND sender_id = ? AND is_read = 0
");
$stmt->execute([$userId, $otherUserId]);
}
// Delete message
public function delete($messageId, $userId) {
$stmt = $this->pdo->prepare("
UPDATE messages
SET is_deleted = 1
WHERE id = ? AND sender_id = ?
");
return $stmt->execute([$messageId, $userId]);
}
}
?>
classes/Notification.php
<?php
class Notification {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
// Get user notifications
public function getUserNotifications($userId, $limit = 50, $offset = 0) {
$stmt = $this->pdo->prepare("
SELECT n.*,
u.username as actor_username,
u.full_name as actor_name,
u.profile_pic as actor_pic
FROM notifications n
LEFT JOIN users u ON n.actor_id = u.id
WHERE n.user_id = ?
ORDER BY n.created_at DESC
LIMIT ? OFFSET ?
");
$stmt->execute([$userId, $limit, $offset]);
return $stmt->fetchAll();
}
// Get unread count
public function getUnreadCount($userId) {
$stmt = $this->pdo->prepare("
SELECT COUNT(*) as count
FROM notifications
WHERE user_id = ? AND is_read = 0
");
$stmt->execute([$userId]);
$result = $stmt->fetch();
return $result['count'];
}
// Mark as read
public function markAsRead($notificationId, $userId) {
$stmt = $this->pdo->prepare("
UPDATE notifications
SET is_read = 1
WHERE id = ? AND user_id = ?
");
return $stmt->execute([$notificationId, $userId]);
}
// Mark all as read
public function markAllAsRead($userId) {
$stmt = $this->pdo->prepare("
UPDATE notifications
SET is_read = 1
WHERE user_id = ? AND is_read = 0
");
return $stmt->execute([$userId]);
}
// Create notification (static wrapper)
public static function create($pdo, $userId, $actorId, $type, $referenceId = null, $content = null) {
$stmt = $pdo->prepare("
INSERT INTO notifications (user_id, actor_id, type, reference_id, content)
VALUES (?, ?, ?, ?, ?)
");
return $stmt->execute([$userId, $actorId, $type, $referenceId, $content]);
}
}
?>
API ENDPOINTS
api/auth.php
<?php
require_once '../config.php';
$action = $_GET['action'] ?? '';
switch ($action) {
case 'login':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$data = json_decode(file_get_contents('php://input'), true);
$user = new User($pdo);
$result = $user->login($data['username'], $data['password']);
jsonResponse($result);
break;
case 'register':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$data = json_decode(file_get_contents('php://input'), true);
$user = new User($pdo);
$result = $user->register($data);
jsonResponse($result);
break;
case 'logout':
session_destroy();
jsonResponse(['success' => true]);
break;
case 'check':
jsonResponse([
'logged_in' => isLoggedIn(),
'user' => getCurrentUser()
]);
break;
default:
jsonResponse(['success' => false, 'message' => 'Invalid action'], 400);
}
?>
api/posts.php
<?php
require_once '../config.php';
if (!isLoggedIn()) {
jsonResponse(['success' => false, 'message' => 'Not authenticated'], 401);
}
$action = $_GET['action'] ?? '';
$userId = getCurrentUserId();
switch ($action) {
case 'create':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$post = new Post($pdo);
$result = $post->create($userId, $_POST, $_FILES);
jsonResponse($result);
break;
case 'feed':
$limit = $_GET['limit'] ?? 20;
$offset = $_GET['offset'] ?? 0;
$post = new Post($pdo);
$posts = $post->getFeed($userId, $limit, $offset);
jsonResponse(['success' => true, 'posts' => $posts]);
break;
case 'get':
$postId = $_GET['id'] ?? 0;
$post = new Post($pdo);
$postData = $post->getById($postId, $userId);
if ($postData) {
jsonResponse(['success' => true, 'post' => $postData]);
} else {
jsonResponse(['success' => false, 'message' => 'Post not found'], 404);
}
break;
case 'like':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$data = json_decode(file_get_contents('php://input'), true);
$post = new Post($pdo);
$result = $post->like($data['post_id'], $userId, $data['reaction'] ?? 'like');
jsonResponse(['success' => true, 'result' => $result]);
break;
case 'comment':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$data = json_decode(file_get_contents('php://input'), true);
$post = new Post($pdo);
$comment = $post->addComment($data['post_id'], $userId, $data['content'], $data['parent_id'] ?? null);
if ($comment) {
jsonResponse(['success' => true, 'comment' => $comment]);
} else {
jsonResponse(['success' => false, 'message' => 'Failed to add comment']);
}
break;
case 'comments':
$postId = $_GET['post_id'] ?? 0;
$post = new Post($pdo);
$comments = $post->getComments($postId);
jsonResponse(['success' => true, 'comments' => $comments]);
break;
case 'share':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$data = json_decode(file_get_contents('php://input'), true);
$post = new Post($pdo);
$result = $post->share($data['post_id'], $userId, $data['content'] ?? null, $data['visibility'] ?? 'public');
jsonResponse(['success' => $result]);
break;
default:
jsonResponse(['success' => false, 'message' => 'Invalid action'], 400);
}
?>
api/recommendations.php
<?php
require_once '../config.php';
if (!isLoggedIn()) {
jsonResponse(['success' => false, 'message' => 'Not authenticated'], 401);
}
$action = $_GET['action'] ?? 'feed';
$userId = getCurrentUserId();
$recommender = new SocialRecommender($pdo);
switch ($action) {
case 'feed':
$limit = $_GET['limit'] ?? 20;
$recommendations = $recommender->getRecommendations($userId, 'feed', $limit);
jsonResponse(['success' => true, 'recommendations' => $recommendations]);
break;
case 'friends':
$limit = $_GET['limit'] ?? 10;
$recommendations = $recommender->getRecommendations($userId, 'friends', $limit);
jsonResponse(['success' => true, 'recommendations' => $recommendations]);
break;
case 'trending':
$limit = $_GET['limit'] ?? 20;
$recommendations = $recommender->getRecommendations($userId, 'trending', $limit);
jsonResponse(['success' => true, 'recommendations' => $recommendations]);
break;
case 'hashtags':
$limit = $_GET['limit'] ?? 10;
$hashtags = $recommender->getTrendingHashtags($limit);
jsonResponse(['success' => true, 'hashtags' => $hashtags]);
break;
default:
jsonResponse(['success' => false, 'message' => 'Invalid action'], 400);
}
?>
api/users.php
<?php
require_once '../config.php';
if (!isLoggedIn()) {
jsonResponse(['success' => false, 'message' => 'Not authenticated'], 401);
}
$action = $_GET['action'] ?? '';
$userId = getCurrentUserId();
switch ($action) {
case 'profile':
$profileId = $_GET['id'] ?? $userId;
$user = new User($pdo);
$profile = $user->getById($profileId);
if ($profile) {
// Check friendship status
$profile['friendship_status'] = getFriendshipStatus($userId, $profileId);
$profile['is_following'] = isFollowing($userId, $profileId);
$profile['mutual_friends'] = getMutualFriendsCount($userId, $profileId);
jsonResponse(['success' => true, 'profile' => $profile]);
} else {
jsonResponse(['success' => false, 'message' => 'User not found'], 404);
}
break;
case 'search':
$query = $_GET['q'] ?? '';
$user = new User($pdo);
$results = $user->search($query);
jsonResponse(['success' => true, 'users' => $results]);
break;
case 'friends':
$targetId = $_GET['id'] ?? $userId;
$user = new User($pdo);
$friends = $user->getFriends($targetId);
jsonResponse(['success' => true, 'friends' => $friends]);
break;
case 'followers':
$targetId = $_GET['id'] ?? $userId;
$user = new User($pdo);
$followers = $user->getFollowers($targetId);
jsonResponse(['success' => true, 'followers' => $followers]);
break;
case 'following':
$targetId = $_GET['id'] ?? $userId;
$user = new User($pdo);
$following = $user->getFollowing($targetId);
jsonResponse(['success' => true, 'following' => $following]);
break;
case 'update':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$data = json_decode(file_get_contents('php://input'), true);
$user = new User($pdo);
$result = $user->updateProfile($userId, $data);
jsonResponse(['success' => $result]);
break;
case 'upload_profile_pic':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$user = new User($pdo);
$result = $user->updateProfilePic($userId, $_FILES['image']);
jsonResponse(['success' => $result]);
break;
case 'upload_cover':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$user = new User($pdo);
$result = $user->updateCoverPhoto($userId, $_FILES['image']);
jsonResponse(['success' => $result]);
break;
case 'friend_request':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$data = json_decode(file_get_contents('php://input'), true);
$friendId = $data['user_id'];
// Check if request exists
$stmt = $pdo->prepare("
SELECT id FROM friendships
WHERE (user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)
");
$stmt->execute([$userId, $friendId, $friendId, $userId]);
if ($stmt->fetch()) {
jsonResponse(['success' => false, 'message' => 'Friend request already exists']);
}
// Create request
$stmt = $pdo->prepare("
INSERT INTO friendships (user_id, friend_id, status, action_user_id)
VALUES (?, ?, 'pending', ?)
");
if ($stmt->execute([$userId, $friendId, $userId])) {
createNotification($friendId, $userId, 'friend_request');
jsonResponse(['success' => true]);
} else {
jsonResponse(['success' => false]);
}
break;
case 'accept_friend':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$data = json_decode(file_get_contents('php://input'), true);
$friendId = $data['user_id'];
$stmt = $pdo->prepare("
UPDATE friendships
SET status = 'accepted'
WHERE user_id = ? AND friend_id = ? AND status = 'pending'
");
if ($stmt->execute([$friendId, $userId])) {
createNotification($friendId, $userId, 'friend_accept');
jsonResponse(['success' => true]);
} else {
jsonResponse(['success' => false]);
}
break;
case 'follow':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$data = json_decode(file_get_contents('php://input'), true);
$followingId = $data['user_id'];
$stmt = $pdo->prepare("INSERT IGNORE INTO follows (follower_id, following_id) VALUES (?, ?)");
$result = $stmt->execute([$userId, $followingId]);
if ($result) {
createNotification($followingId, $userId, 'follow');
}
jsonResponse(['success' => $result]);
break;
case 'unfollow':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$data = json_decode(file_get_contents('php://input'), true);
$followingId = $data['user_id'];
$stmt = $pdo->prepare("DELETE FROM follows WHERE follower_id = ? AND following_id = ?");
$result = $stmt->execute([$userId, $followingId]);
jsonResponse(['success' => $result]);
break;
default:
jsonResponse(['success' => false, 'message' => 'Invalid action'], 400);
}
?>
api/messages.php
<?php
require_once '../config.php';
if (!isLoggedIn()) {
jsonResponse(['success' => false, 'message' => 'Not authenticated'], 401);
}
$action = $_GET['action'] ?? '';
$userId = getCurrentUserId();
$message = new Message($pdo);
switch ($action) {
case 'send':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$receiverId = $_POST['receiver_id'];
$content = $_POST['content'] ?? '';
$file = $_FILES['file'] ?? null;
$result = $message->send($userId, $receiverId, $content, $file);
if ($result) {
jsonResponse(['success' => true, 'message' => $result]);
} else {
jsonResponse(['success' => false, 'message' => 'Failed to send message']);
}
break;
case 'conversation':
$otherUserId = $_GET['user_id'] ?? 0;
$limit = $_GET['limit'] ?? 50;
$before = $_GET['before'] ?? null;
$messages = $message->getConversation($userId, $otherUserId, $limit, $before);
jsonResponse(['success' => true, 'messages' => $messages]);
break;
case 'list':
$conversations = $message->getConversations($userId);
jsonResponse(['success' => true, 'conversations' => $conversations]);
break;
case 'delete':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$data = json_decode(file_get_contents('php://input'), true);
$result = $message->delete($data['message_id'], $userId);
jsonResponse(['success' => $result]);
break;
default:
jsonResponse(['success' => false, 'message' => 'Invalid action'], 400);
}
?>
api/notifications.php
<?php
require_once '../config.php';
if (!isLoggedIn()) {
jsonResponse(['success' => false, 'message' => 'Not authenticated'], 401);
}
$action = $_GET['action'] ?? '';
$userId = getCurrentUserId();
$notification = new Notification($pdo);
switch ($action) {
case 'list':
$limit = $_GET['limit'] ?? 50;
$offset = $_GET['offset'] ?? 0;
$notifications = $notification->getUserNotifications($userId, $limit, $offset);
$unreadCount = $notification->getUnreadCount($userId);
jsonResponse([
'success' => true,
'notifications' => $notifications,
'unread_count' => $unreadCount
]);
break;
case 'mark_read':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
}
$data = json_decode(file_get_contents('php://input'), true);
if (isset($data['all']) && $data['all']) {
$result = $notification->markAllAsRead($userId);
} else {
$result = $notification->markAsRead($data['notification_id'], $userId);
}
jsonResponse(['success' => $result]);
break;
case 'count':
$count = $notification->getUnreadCount($userId);
jsonResponse(['success' => true, 'count' => $count]);
break;
default:
jsonResponse(['success' => false, 'message' => 'Invalid action'], 400);
}
?>
api/search.php
<?php
require_once '../config.php';
if (!isLoggedIn()) {
jsonResponse(['success' => false, 'message' => 'Not authenticated'], 401);
}
$query = $_GET['q'] ?? '';
$type = $_GET['type'] ?? 'all';
$limit = $_GET['limit'] ?? 20;
$offset = $_GET['offset'] ?? 0;
if (empty($query)) {
jsonResponse(['success' => false, 'message' => 'Search query required'], 400);
}
$results = [];
// Search users
if ($type == 'all' || $type == 'users') {
$user = new User($pdo);
$results['users'] = $user->search($query, $limit, $offset);
}
// Search posts
if ($type == 'all' || $type == 'posts') {
$stmt = $pdo->prepare("
SELECT p.*, u.username, u.full_name, u.profile_pic
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE MATCH(p.content) AGAINST(?)
AND p.status = 'active'
AND p.visibility = 'public'
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?
");
$stmt->execute([$query, $limit, $offset]);
$results['posts'] = $stmt->fetchAll();
}
// Search hashtags
if ($type == 'all' || $type == 'hashtags') {
$stmt = $pdo->prepare("
SELECT * FROM hashtags
WHERE tag LIKE ?
ORDER BY posts_count DESC
LIMIT ? OFFSET ?
");
$stmt->execute(["%$query%", $limit, $offset]);
$results['hashtags'] = $stmt->fetchAll();
}
jsonResponse(['success' => true, 'results' => $results]);
?>
FRONTEND IMPLEMENTATION
index.html (Main Application)
```html
SocialConnect - Connect with the World
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Custom CSS -->
<style>
/* Custom animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fadeIn 0.3s ease-out;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Post hover effects */
.post-card {
transition: transform 0.2s, box-shadow 0.2s;
}
.post-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
}
/* Loading spinner */
.loading-spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #3b82f6;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Story ring animation */
.story-ring {
background: linear-gradient(45deg, #f09433, #d
Social Media Platform - Frontend Implementation (Continued)
background: linear-gradient(45deg, #f09433, #d66837, #dc2743, #cc2366, #bc1888);
padding: 2px;
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
/* Message bubble */
.message-bubble-sent {
background: #3b82f6;
color: white;
border-radius: 18px 18px 4px 18px;
}
.message-bubble-received {
background: #f3f4f6;
color: #1f2937;
border-radius: 18px 18px 18px 4px;
}
/* Notification badge */
.notification-badge {
position: absolute;
top: -5px;
right: -5px;
background: #ef4444;
color: white;
border-radius: 50%;
min-width: 18px;
height: 18px;
font-size: 11px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
}
/* Mobile menu */
.mobile-menu-enter {
transform: translateX(-100%);
}
.mobile-menu-enter-active {
transform: translateX(0);
transition: transform 0.3s ease-out;
}
</style>
</head>
<body class="bg-gray-100">
<!-- Loading Overlay -->
<div id="loadingOverlay" class="fixed inset-0 bg-white z-50 flex items-center justify-center hidden">
<div class="loading-spinner"></div>
</div>
<!-- Navbar -->
<nav class="bg-white shadow-lg sticky top-0 z-40">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<!-- Logo and brand -->
<div class="flex items-center">
<div class="flex-shrink-0 flex items-center cursor-pointer" onclick="window.location='index.html'">
<i class="fab fa-connectdevelop text-3xl text-blue-600"></i>
<span class="ml-2 text-xl font-bold text-gray-800 hidden sm:block">SocialConnect</span>
</div>
<!-- Search bar -->
<div class="hidden md:block ml-6">
<div class="relative">
<input type="text"
id="searchInput"
placeholder="Search people, posts, hashtags..."
class="w-96 px-4 py-2 pl-10 pr-4 rounded-full border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
onkeypress="if(event.key === 'Enter') search()">
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
</div>
</div>
</div>
<!-- Navigation links (desktop) -->
<div class="hidden md:flex items-center space-x-4">
<a href="#" onclick="loadHome()" class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<i class="fas fa-home"></i>
<span class="ml-1">Home</span>
</a>
<a href="#" onclick="loadFriends()" class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<i class="fas fa-users"></i>
<span class="ml-1">Friends</span>
</a>
<a href="#" onclick="loadMessages()" class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50 relative">
<i class="fas fa-comment"></i>
<span class="ml-1">Messages</span>
<span id="messageBadge" class="notification-badge hidden">0</span>
</a>
<a href="#" onclick="loadNotifications()" class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50 relative">
<i class="fas fa-bell"></i>
<span class="ml-1">Notifications</span>
<span id="notificationBadge" class="notification-badge hidden">0</span>
</a>
<!-- Profile dropdown -->
<div class="relative" id="profileDropdown">
<button onclick="toggleProfileDropdown()" class="flex items-center space-x-2 focus:outline-none">
<img id="navProfilePic" src="assets/images/default-avatar.png" alt="Profile" class="w-8 h-8 rounded-full object-cover">
<span id="navUsername" class="text-sm font-medium text-gray-700">Loading...</span>
<i class="fas fa-chevron-down text-gray-500 text-xs"></i>
</button>
<div id="profileMenu" class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50">
<a href="#" onclick="loadProfile()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-user mr-2"></i>Profile
</a>
<a href="#" onclick="loadSettings()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-cog mr-2"></i>Settings
</a>
<hr class="my-1">
<a href="#" onclick="logout()" class="block px-4 py-2 text-sm text-red-600 hover:bg-gray-100">
<i class="fas fa-sign-out-alt mr-2"></i>Logout
</a>
</div>
</div>
</div>
<!-- Mobile menu button -->
<div class="flex items-center md:hidden">
<button onclick="toggleMobileMenu()" class="inline-flex items-center justify-center p-2 rounded-md text-gray-700 hover:text-gray-900 hover:bg-gray-100 focus:outline-none">
<i class="fas fa-bars text-2xl"></i>
</button>
</div>
</div>
</div>
<!-- Mobile menu -->
<div id="mobileMenu" class="hidden md:hidden bg-white border-t border-gray-200 py-2">
<div class="px-4 py-2">
<div class="relative">
<input type="text"
id="mobileSearchInput"
placeholder="Search..."
class="w-full px-4 py-2 pl-10 pr-4 rounded-full border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
onkeypress="if(event.key === 'Enter') search()">
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
</div>
</div>
<a href="#" onclick="loadHome()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-home w-6"></i> Home
</a>
<a href="#" onclick="loadFriends()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-users w-6"></i> Friends
</a>
<a href="#" onclick="loadMessages()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 relative">
<i class="fas fa-comment w-6"></i> Messages
<span id="mobileMessageBadge" class="notification-badge hidden">0</span>
</a>
<a href="#" onclick="loadNotifications()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 relative">
<i class="fas fa-bell w-6"></i> Notifications
<span id="mobileNotificationBadge" class="notification-badge hidden">0</span>
</a>
<hr class="my-2">
<a href="#" onclick="loadProfile()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-user w-6"></i> Profile
</a>
<a href="#" onclick="loadSettings()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-cog w-6"></i> Settings
</a>
<a href="#" onclick="logout()" class="block px-4 py-2 text-sm text-red-600 hover:bg-gray-100">
<i class="fas fa-sign-out-alt w-6"></i> Logout
</a>
</div>
</nav>
<!-- Main Content -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="flex flex-col lg:flex-row gap-6">
<!-- Left Sidebar (desktop) -->
<div class="hidden lg:block lg:w-64 space-y-4">
<!-- User info card -->
<div class="bg-white rounded-lg shadow p-4">
<div class="flex items-center space-x-3">
<img id="sidebarProfilePic" src="assets/images/default-avatar.png" alt="Profile" class="w-12 h-12 rounded-full object-cover">
<div>
<h3 id="sidebarName" class="font-semibold">Loading...</h3>
<p id="sidebarUsername" class="text-sm text-gray-500">@loading</p>
</div>
</div>
<div class="mt-4 grid grid-cols-3 gap-2 text-center text-sm">
<div>
<div id="sidebarPostsCount" class="font-semibold">0</div>
<div class="text-gray-500">Posts</div>
</div>
<div>
<div id="sidebarFriendsCount" class="font-semibold">0</div>
<div class="text-gray-500">Friends</div>
</div>
<div>
<div id="sidebarFollowersCount" class="font-semibold">0</div>
<div class="text-gray-500">Followers</div>
</div>
</div>
</div>
<!-- Trending topics -->
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold mb-3 flex items-center">
<i class="fas fa-fire text-orange-500 mr-2"></i>
Trending
</h3>
<div id="trendingList" class="space-y-2">
<!-- Loaded via JavaScript -->
</div>
</div>
<!-- Friend suggestions -->
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold mb-3 flex items-center">
<i class="fas fa-user-friends text-blue-500 mr-2"></i>
Suggested Friends
</h3>
<div id="friendSuggestions" class="space-y-3">
<!-- Loaded via JavaScript -->
</div>
<a href="#" onclick="loadMoreSuggestions()" class="block text-center text-sm text-blue-600 hover:text-blue-800 mt-3">
See More
</a>
</div>
</div>
<!-- Main Content Area -->
<div class="flex-1">
<div id="contentArea">
<!-- Content loaded dynamically -->
</div>
</div>
<!-- Right Sidebar (desktop) -->
<div class="hidden lg:block lg:w-80 space-y-4">
<!-- Create post -->
<div class="bg-white rounded-lg shadow p-4">
<div class="flex items-center space-x-3">
<img id="createPostAvatar" src="assets/images/default-avatar.png" alt="Profile" class="w-10 h-10 rounded-full object-cover">
<button onclick="openCreatePostModal()" class="flex-1 text-left px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-full text-gray-500 text-sm">
What's on your mind?
</button>
</div>
<div class="mt-3 flex justify-around">
<button onclick="openCreatePostModal('photo')" class="flex items-center space-x-1 text-gray-600 hover:text-blue-600">
<i class="fas fa-image text-green-500"></i>
<span class="text-sm">Photo</span>
</button>
<button onclick="openCreatePostModal('video')" class="flex items-center space-x-1 text-gray-600 hover:text-blue-600">
<i class="fas fa-video text-blue-500"></i>
<span class="text-sm">Video</span>
</button>
<button onclick="openCreatePostModal()" class="flex items-center space-x-1 text-gray-600 hover:text-blue-600">
<i class="fas fa-smile text-yellow-500"></i>
<span class="text-sm">Feeling</span>
</button>
</div>
</div>
<!-- Stories -->
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold mb-3 flex items-center justify-between">
<span>Stories</span>
<a href="#" class="text-sm text-blue-600 hover:text-blue-800">See All</a>
</h3>
<div id="storiesList" class="space-y-3">
<!-- Loaded via JavaScript -->
</div>
</div>
<!-- Upcoming birthdays -->
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold mb-3 flex items-center">
<i class="fas fa-gift text-pink-500 mr-2"></i>
Birthdays
</h3>
<div id="birthdaysList">
<!-- Loaded via JavaScript -->
</div>
</div>
</div>
</div>
</div>
<!-- Create Post Modal -->
<div id="createPostModal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden items-center justify-center">
<div class="bg-white rounded-lg w-full max-w-lg mx-4 animate-fade-in">
<div class="flex justify-between items-center p-4 border-b">
<h3 class="text-lg font-semibold">Create Post</h3>
<button onclick="closeCreatePostModal()" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<div class="p-4">
<div class="flex items-center space-x-3 mb-4">
<img id="modalProfilePic" src="assets/images/default-avatar.png" alt="Profile" class="w-10 h-10 rounded-full object-cover">
<div>
<h4 id="modalName" class="font-semibold">Loading...</h4>
<select id="postVisibility" class="text-sm border rounded-md px-2 py-1">
<option value="public">Public</option>
<option value="friends">Friends</option>
<option value="private">Only Me</option>
</select>
</div>
</div>
<textarea id="postContent" rows="4" class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="What's on your mind?"></textarea>
<div id="mediaPreview" class="mt-4 grid grid-cols-2 gap-2 hidden">
<!-- Media preview will be shown here -->
</div>
<div class="mt-4 border rounded-lg p-3">
<div class="flex justify-between items-center">
<span class="font-medium">Add to your post</span>
<div class="flex space-x-2">
<label class="cursor-pointer">
<i class="fas fa-image text-green-500 text-xl hover:opacity-75"></i>
<input type="file" id="postImage" accept="image/*" multiple class="hidden" onchange="previewMedia()">
</label>
<label class="cursor-pointer">
<i class="fas fa-video text-blue-500 text-xl hover:opacity-75"></i>
<input type="file" id="postVideo" accept="video/*" class="hidden" onchange="previewMedia()">
</label>
<label class="cursor-pointer">
<i class="fas fa-smile text-yellow-500 text-xl hover:opacity-75"></i>
<input type="file" id="postAudio" accept="audio/*" class="hidden" onchange="previewMedia()">
</label>
</div>
</div>
</div>
</div>
<div class="p-4 border-t">
<button onclick="createPost()" class="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition">
Post
</button>
</div>
</div>
</div>
<!-- Login Modal -->
<div id="loginModal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden items-center justify-center">
<div class="bg-white rounded-lg w-full max-w-md mx-4 p-6 animate-fade-in">
<div class="text-center mb-6">
<i class="fab fa-connectdevelop text-5xl text-blue-600"></i>
<h2 class="text-2xl font-bold mt-2">Welcome Back</h2>
<p class="text-gray-600">Sign in to continue</p>
</div>
<form onsubmit="login(event)">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Username or Email</label>
<input type="text" id="loginUsername" required
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Password</label>
<input type="password" id="loginPassword" required
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<button type="submit" class="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition mb-4">
Sign In
</button>
<p class="text-center text-sm text-gray-600">
Don't have an account?
<a href="#" onclick="showRegisterModal()" class="text-blue-600 hover:text-blue-800">Sign Up</a>
</p>
</form>
</div>
</div>
<!-- Register Modal -->
<div id="registerModal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden items-center justify-center">
<div class="bg-white rounded-lg w-full max-w-md mx-4 p-6 animate-fade-in">
<div class="text-center mb-6">
<i class="fab fa-connectdevelop text-5xl text-blue-600"></i>
<h2 class="text-2xl font-bold mt-2">Create Account</h2>
<p class="text-gray-600">Join our community</p>
</div>
<form onsubmit="register(event)">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Username</label>
<input type="text" id="regUsername" required
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Email</label>
<input type="email" id="regEmail" required
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Full Name</label>
<input type="text" id="regFullName" required
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Password</label>
<input type="password" id="regPassword" required
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Birth Date</label>
<input type="date" id="regBirthDate" required
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Gender</label>
<select id="regGender" class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="male">Male</option>
<option value="female">Female</option>
<option value="other">Other</option>
<option value="prefer_not_to_say">Prefer not to say</option>
</select>
</div>
<button type="submit" class="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition mb-4">
Sign Up
</button>
<p class="text-center text-sm text-gray-600">
Already have an account?
<a href="#" onclick="showLoginModal()" class="text-blue-600 hover:text-blue-800">Sign In</a>
</p>
</form>
</div>
</div>
<!-- JavaScript -->
<script>
// State management
let currentUser = null;
let currentPage = 'home';
let ws = null;
let notificationInterval = null;
// Initialize on load
document.addEventListener('DOMContentLoaded', function() {
checkAuth();
initializeWebSocket();
startPolling();
});
// Check authentication
function checkAuth() {
showLoading();
fetch('api/auth.php?action=check')
.then(response => response.json())
.then(data => {
if (data.logged_in) {
currentUser = data.user;
updateUI();
loadHome();
} else {
showLoginModal();
}
})
.finally(() => {
hideLoading();
});
}
// Login
function login(event) {
event.preventDefault();
showLoading();
const username = document.getElementById('loginUsername').value;
const password = document.getElementById('loginPassword').value;
fetch('api/auth.php?action=login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password })
})
.then(response => response.json())
.then(data => {
if (data.success) {
closeModals();
currentUser = data.user;
updateUI();
loadHome();
initializeWebSocket();
} else {
alert('Login failed: ' + data.message);
}
})
.finally(() => {
hideLoading();
});
}
// Register
function register(event) {
event.preventDefault();
showLoading();
const userData = {
username: document.getElementById('regUsername').value,
email: document.getElementById('regEmail').value,
full_name: document.getElementById('regFullName').value,
password: document.getElementById('regPassword').value,
birth_date: document.getElementById('regBirthDate').value,
gender: document.getElementById('regGender').value
};
fetch('api/auth.php?action=register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
closeModals();
showLoginModal();
alert('Registration successful! Please login.');
} else {
alert('Registration failed: ' + data.message);
}
})
.finally(() => {
hideLoading();
});
}
// Logout
function logout() {
fetch('api/auth.php?action=logout')
.then(() => {
currentUser = null;
if (ws) {
ws.close();
}
if (notificationInterval) {
clearInterval(notificationInterval);
}
showLoginModal();
});
}
// Update UI with user data
function updateUI() {
if (!currentUser) return;
// Update profile pictures
document.getElementById('navProfilePic').src = currentUser.profile_pic || 'assets/images/default-avatar.png';
document.getElementById('sidebarProfilePic').src = currentUser.profile_pic || 'assets/images/default-avatar.png';
document.getElementById('createPostAvatar').src = currentUser.profile_pic || 'assets/images/default-avatar.png';
document.getElementById('modalProfilePic').src = currentUser.profile_pic || 'assets/images/default-avatar.png';
// Update names
document.getElementById('navUsername').textContent = currentUser.full_name || currentUser.username;
document.getElementById('sidebarName').textContent = currentUser.full_name || currentUser.username;
document.getElementById('sidebarUsername').textContent = '@' + currentUser.username;
document.getElementById('modalName').textContent = currentUser.full_name || currentUser.username;
// Update counts
document.getElementById('sidebarPostsCount').textContent = currentUser.posts_count || 0;
document.getElementById('sidebarFriendsCount').textContent = currentUser.friends_count || 0;
document.getElementById('sidebarFollowersCount').textContent = currentUser.followers_count || 0;
}
// Load home feed
function loadHome() {
currentPage = 'home';
showLoading();
Promise.all([
fetch('api/posts.php?action=feed').then(r => r.json()),
fetch('api/recommendations.php?action=feed').then(r => r.json()),
fetch('api/recommendations.php?action=trending').then(r => r.json()),
loadStories()
])
.then(([feedData, recData, trendingData]) => {
const content = document.getElementById('contentArea');
content.innerHTML = `
<div class="space-y-6">
<!-- Create post (mobile) -->
<div class="lg:hidden bg-white rounded-lg shadow p-4">
<div class="flex items-center space-x-3">
<img src="${currentUser.profile_pic || 'assets/images/default-avatar.png'}" class="w-10 h-10 rounded-full object-cover">
<button onclick="openCreatePostModal()" class="flex-1 text-left px-4 py-2 bg-gray-100 rounded-full text-gray-500 text-sm">
What's on your mind?
</button>
</div>
</div>
<!-- Stories (mobile) -->
<div class="lg:hidden bg-white rounded-lg shadow p-4">
<div class="flex justify-between items-center mb-3">
<h3 class="font-semibold">Stories</h3>
<a href="#" class="text-sm text-blue-600">See All</a>
</div>
<div id="mobileStories" class="flex space-x-4 overflow-x-auto pb-2">
${renderStoriesHTML()}
</div>
</div>
<!-- Feed Tabs -->
<div class="bg-white rounded-lg shadow">
<div class="flex border-b">
<button onclick="switchFeed('latest')" class="flex-1 px-4 py-3 text-sm font-medium ${getActiveFeedTab('latest')}">
Latest
</button>
<button onclick="switchFeed('recommended')" class="flex-1 px-4 py-3 text-sm font-medium ${getActiveFeedTab('recommended')}">
Recommended
</button>
<button onclick="switchFeed('trending')" class="flex-1 px-4 py-3 text-sm font-medium ${getActiveFeedTab('trending')}">
Trending
</button>
</div>
<div id="feedContent" class="p-4">
${renderFeed(feedData.posts)}
</div>
<div class="p-4 border-t text-center">
<button onclick="loadMorePosts()" class="text-blue-600 hover:text-blue-800 text-sm">
Load More
</button>
</div>
</div>
</div>
`;
// Load recommendations into sidebar
loadSidebarContent(recData, trendingData);
})
.finally(() => {
hideLoading();
});
}
// Render feed posts
function renderFeed(posts) {
if (!posts || posts.length === 0) {
return '<p class="text-center text-gray-500 py-8">No posts to show</p>';
}
return posts.map(post => `
<div class="post-card bg-white rounded-lg shadow mb-4 p-4" data-post-id="${post.id}">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center space-x-3">
<img src="${post.profile_pic || 'assets/images/default-avatar.png'}"
alt="${post.full_name}"
class="w-10 h-10 rounded-full object-cover cursor-pointer"
onclick="loadUserProfile(${post.user_id})">
<div>
<h4 class="font-semibold cursor-pointer hover:underline" onclick="loadUserProfile(${post.user_id})">
${post.full_name}
</h4>
<p class="text-xs text-gray-500">${timeAgo(post.created_at)}</p>
</div>
</div>
<button onclick="showPostMenu(${post.id})" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-ellipsis-h"></i>
</button>
</div>
<div class="mb-3">
<p class="text-gray-800">${post.content}</p>
</div>
${renderPostMedia(post)}
<div class="flex items-center justify-between mt-3 pt-3 border-t">
<button onclick="likePost(${post.id})" class="flex items-center space-x-1 text-gray-600 hover:text-blue-600">
<i class="fas fa-thumbs-up ${post.user_liked ? 'text-blue-600' : ''}"></i>
<span>${post.likes_count || 0}</span>
</button>
<button onclick="showComments(${post.id})" class="flex items-center space-x-1 text-gray-600 hover:text-blue-600">
<i class="fas fa-comment"></i>
<span>${post.comments_count || 0}</span>
</button>
<button onclick="sharePost(${post.id})" class="flex items-center space-x-1 text-gray-600 hover:text-blue-600">
<i class="fas fa-share"></i>
<span>${post.shares_count || 0}</span>
</button>
</div>
<!-- Comments section (hidden by default) -->
<div id="comments-${post.id}" class="hidden mt-4 pt-4 border-t">
<div class="space-y-3" id="comments-list-${post.id}">
<!-- Comments loaded dynamically -->
</div>
<div class="flex items-center space-x-2 mt-3">
<img src="${currentUser.profile_pic || 'assets/images/default-avatar.png'}" class="w-8 h-8 rounded-full object-cover">
<input type="text" id="comment-${post.id}" placeholder="Write a comment..."
class="flex-1 px-3 py-2 bg-gray-100 rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
onkeypress="if(event.key === 'Enter') addComment(${post.id})">
</div>
</div>
</div>
`).join('');
}
// Render post media
function renderPostMedia(post) {
if (!post.media_url || post.media_url.length === 0) return '';
const media = JSON.parse(post.media_url);
if (media.length === 0) return '';
if (post.media_type === 'image') {
if (media.length === 1) {
return `
<div class="mb-3 rounded-lg overflow-hidden">
<img src="${media[0]}" alt="Post image" class="w-full object-cover">
</div>
`;
} else {
return `
<div class="mb-3 grid grid-cols-${Math.min(media.length, 2)} gap-1">
${media.map(url => `
<img src="${url}" alt="Post image" class="w-full h-48 object-cover rounded">
`).join('')}
</div>
`;
}
}
if (post.media_type === 'video') {
return `
<div class="mb-3 rounded-lg overflow-hidden">
<video src="${media[0]}" controls class="w-full"></video>
</div>
`;
}
return '';
}
// Load sidebar content
function loadSidebarContent(recData, trendingData) {
// Update trending
const trendingList = document.getElementById('trendingList');
if (trendingList) {
trendingList.innerHTML = trendingData.trending?.map(item => `
<a href="#" onclick="searchHashtag('${item.tag}')" class="block hover:bg-gray-50 p-2 rounded">
<p class="font-medium text-blue-600">#${item.tag}</p>
<p class="text-xs text-gray-500">${item.posts_count} posts</p>
</a>
`).join('') || '<p class="text-gray-500 text-sm">No trending topics</p>';
}
// Update friend suggestions
const suggestions = document.getElementById('friendSuggestions');
if (suggestions && recData.recommendations) {
suggestions.innerHTML = recData.recommendations.map(suggestion => `
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<img src="${suggestion.user.profile_pic || 'assets/images/default-avatar.png'}"
alt="${suggestion.user.full_name}"
class="w-8 h-8 rounded-full object-cover cursor-pointer"
onclick="loadUserProfile(${suggestion.user.id})">
<div>
<p class="text-sm font-medium cursor-pointer hover:underline" onclick="loadUserProfile(${suggestion.user.id})">
${suggestion.user.full_name}
</p>
<p class="text-xs text-gray-500">${suggestion.reasons?.join(' • ') || ''}</p>
</div>
</div>
<button onclick="sendFriendRequest(${suggestion.user.id})" class="text-blue-600 hover:text-blue-800">
<i class="fas fa-user-plus"></i>
</button>
</div>
`).join('');
}
}
// Load stories
function loadStories() {
// Mock stories for now
return Promise.resolve();
}
// Render stories HTML
function renderStoriesHTML() {
const stories = [
{ id: 1, name: 'Your Story', image: currentUser.profile_pic, isUser: true },
{ id: 2, name: 'John Doe', image: null },
{ id: 3, name: 'Jane Smith', image: null },
{ id: 4, name: 'Mike Johnson', image: null }
];
return stories.map(story => `
<div class="flex-shrink-0 text-center">
<div class="story-ring">
<img src="${story.image || 'assets/images/default-avatar.png'}"
alt="${story.name}"
class="w-14 h-14 rounded-full object-cover border-2 border-white">
</div>
<p class="text-xs mt-1 max-w-[80px] truncate">${story.name}</p>
</div>
`).join('');
}
// Load user profile
function loadUserProfile(userId) {
showLoading();
fetch(`api/users.php?action=profile&id=${userId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
displayUserProfile(data.profile);
}
})
.finally(() => {
hideLoading();
});
}
// Display user profile
function displayUserProfile(profile) {
const content = document.getElementById('contentArea');
content.innerHTML = `
<div class="bg-white rounded-lg shadow overflow-hidden">
<!-- Cover Photo -->
<div class="h-48 bg-gradient-to-r from-blue-500 to-purple-600 relative">
<img src="${profile.cover_photo || 'assets/images/default-cover.jpg'}"
alt="Cover"
class="w-full h-full object-cover">
<!-- Profile Picture -->
<div class="absolute -bottom-16 left-8">
<div class="relative">
<img src="${profile.profile_pic || 'assets/images/default-avatar.png'}"
alt="${profile.full_name}"
class="w-32 h-32 rounded-full border-4 border-white object-cover">
${profile.user_id === currentUser.id ? `
<label class="absolute bottom-0 right-0 bg-blue-600 text-white p-2 rounded-full cursor-pointer hover:bg-blue-700">
<i class="fas fa-camera text-sm"></i>
<input type="file" id="profilePicUpload" accept="image/*" class="hidden" onchange="uploadProfilePic()">
</label>
` : ''}
</div>
</div>
<!-- Cover Photo Edit (if own profile) -->
${profile.user_id === currentUser.id ? `
<label class="absolute top-4 right-4 bg-black bg-opacity-50 text-white px-4 py-2 rounded-lg cursor-pointer hover:bg-opacity-70">
<i class="fas fa-camera mr-2"></i>Edit Cover
<input type="file" id="coverPhotoUpload" accept="image/*" class="hidden" onchange="uploadCoverPhoto()">
</label>
` : ''}
</div>
<!-- Profile Info -->
<div class="pt-20 px-8 pb-6">
<div class="flex justify-between items-start">
<div>
<h2 class="text-2xl font-bold">${profile.full_name}</h2>
<p class="text-gray-600">@${profile.username}</p>
<p class="mt-2 text-gray-700">${profile.bio || 'No bio yet'}</p>
<div class="flex space-x-6 mt-4">
<div>
<span class="font-bold">${profile.posts_count || 0}</span>
<span class="text-gray-600">Posts</span>
</div>
<div>
<span class="font-bold">${profile.friends_count || 0}</span>
<span class="text-gray-600">Friends</span>
</div>
<div>
<span class="font-bold">${profile.followers_count || 0}</span>
<span class="text-gray-600">Followers</span>
</div>
<div>
<span class="font-bold">${profile.following_count || 0}</span>
<span class="text-gray-600">Following</span>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex space-x-2">
${profile.user_id !== currentUser.id ? `
<button onclick="sendFriendRequest(${profile.user_id})" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">
${profile.friendship_status === 'pending' ? 'Request Pending' :
profile.friendship_status === 'accepted' ? 'Friends' : 'Add Friend'}
</button>
<button onclick="sendMessage(${profile.user_id})" class="bg-gray-200 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-300">
Message
</button>
` : `
<button onclick="editProfile()" class="bg-gray-200 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-300">
Edit Profile
</button>
`}
</div>
</div>
</div>
<!-- User Posts -->
<div class="border-t px-8 py-6">
<h3 class="text-lg font-semibold mb-4">Posts</h3>
<div id="userPosts">
<!-- Posts loaded via JavaScript -->
</div>
</div>
</div>
`;
// Load user posts
loadUserPosts(profile.user_id);
}
// Load user posts
function loadUserPosts(userId) {
fetch(`api/posts.php?action=feed&user=${userId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('userPosts').innerHTML = renderFeed(data.posts);
}
});
}
// Load messages
function loadMessages() {
currentPage = 'messages';
showLoading();
fetch('api/messages.php?action=list')
.then(response => response.json())
.then(data => {
if (data.success) {
displayMessages(data.conversations);
}
})
.finally(() => {
hideLoading();
});
}
// Display messages
function displayMessages(conversations) {
const content = document.getElementById('contentArea');
content.innerHTML = `
<div class="bg-white rounded-lg shadow h-[calc(100vh-200px)] flex">
<!-- Conversations List -->
<div class="w-1/3 border-r overflow-y-auto">
<div class="p-4 border-b">
<h3 class="text-lg font-semibold">Messages</h3>
</div>
<div id="conversationsList" class="divide-y">
${renderConversations(conversations)}
</div>
</div>
<!-- Active Conversation -->
<div class="flex-1 flex flex-col">
<div id="activeConversation" class="flex-1 flex items-center justify-center text-gray-500">
Select a conversation to start messaging
</div>
</div>
</div>
`;
}
// Render conversations
function renderConversations(conversations) {
if (!conversations || conversations.length === 0) {
return '<p class="text-center text-gray-500 p-4">No conversations yet</p>';
}
return conversations.map(conv => `
<div class="p-4 hover:bg-gray-50 cursor-pointer" onclick="openConversation(${conv.other_user_id})">
<div class="flex items-center space-x-3">
<img src="${conv.profile_pic || 'assets/images/default-avatar.png'}"
alt="${conv.full_name}"
class="w-12 h-12 rounded-full object-cover">
<div class="flex-1 min-w-0">
<div class="flex justify-between items-baseline">
<h4 class="font-semibold truncate">${conv.full_name}</h4>
<span class="text-xs text-gray-500">${timeAgo(conv.last_message_time)}</span>
</div>
<p class="text-sm text-gray-600 truncate">${conv.last_message || ''}</p>
</div>
${conv.unread_count > 0 ? `
<span class="bg-blue-600 text-white text-xs px-2 py-1 rounded-full">${conv.unread_count}</span>
` : ''}
</div>
</div>
`).join('');
}
// Open conversation
function openConversation(userId) {
showLoading();
fetch(`api/messages.php?action=conversation&user_id=${userId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
displayConversation(userId, data.messages);
}
})
.finally(() => {
hideLoading();
});
}
// Display conversation
function displayConversation(userId, messages) {
const conversationDiv = document.getElementById('activeConversation');
conversationDiv.innerHTML = `
<div class="flex flex-col h-full">
<!-- Messages -->
<div id="messageList" class="flex-1 overflow-y-auto p-4 space-y-4">
${renderMessages(messages)}
</div>
<!-- Message Input -->
<div class="border-t p-4">
<div class="flex items-center space-x-2">
<input type="text" id="messageInput" placeholder="Type a message..."
class="flex-1 px-4 py-2 border rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500"
onkeypress="if(event.key === 'Enter') sendMessage(${userId})">
<button onclick="sendMessage(${userId})" class="bg-blue-600 text-white p-2 rounded-full hover:bg-blue-700">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
</div>
`;
// Scroll to bottom
const messageList = document.getElementById('messageList');
messageList.scrollTop = messageList.scrollHeight;
}
// Render messages
function renderMessages(messages) {
if (!messages || messages.length === 0) {
return '<p class="text-center text-gray-500">No messages yet. Start the conversation!</p>';
}
return messages.map(msg => `
<div class="flex ${msg.sender_id === currentUser.id ? 'justify-end' : 'justify-start'}">
<div class="max-w-[70%] ${msg.sender_id === currentUser.id ? 'message-bubble-sent' : 'message-bubble-received'} px-4 py-2">
<p class="text-sm">${msg.content}</p>
<p class="text-xs mt-1 ${msg.sender_id === currentUser.id ? 'text-blue-100' : 'text-gray-500'}">
${timeAgo(msg.created_at)}
</p>
</div>
</div>
`).join('');
}
// Send message
function sendMessage(receiverId) {
const input = document.getElementById('messageInput');
const content = input.value.trim();
if (!content) return;
fetch('api/messages.php?action=send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
receiver_id: receiverId,
content: content
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
input.value = '';
// Add message to list
const messageList = document.getElementById('messageList');
messageList.innerHTML += `
<div class="flex justify-end">
<div class="max-w-[70%] message-bubble-sent px-4 py-2">
<p class="text-sm">${content}</p>
<p class="text-xs mt-1 text-blue-100">Just now</p>
</div>
</div>
`;
messageList.scrollTop = messageList.scrollHeight;
}
});
}
// Load notifications
function loadNotifications() {
currentPage = 'notifications';
showLoading();
fetch('api/notifications.php?action=list')
.then(response => response.json())
.then(data => {
if (data.success) {
displayNotifications(data.notifications);
updateNotificationBadge(0);
}
})
.finally(() => {
hideLoading();
});
}
// Display notifications
function displayNotifications(notifications) {
const content = document.getElementById('contentArea');
content.innerHTML = `
<div class="bg-white rounded-lg shadow">
<div class="p-4 border-b">
<h3 class="text-lg font-semibold">Notifications</h3>
</div>
<div class="divide-y">
${renderNotifications(notifications)}
</div>
${notifications.length === 0 ? `
<div class="p-8 text-center text-gray-500">
No notifications yet
</div>
` : ''}
</div>
`;
}
// Render notifications
function renderNotifications(notifications) {
return notifications.map(notif => `
<div class="p-4 hover:bg-gray-50 ${notif.is_read ? '' : 'bg-blue-50'}" onclick="markNotificationRead(${notif.id})">
<div class="flex items-center space-x-3">
<img src="${notif.actor_pic || 'assets/images/default-avatar.png'}"
alt="${notif.actor_name}"
class="w-10 h-10 rounded-full object-cover">
<div class="flex-1">
<p class="text-sm">
<span class="font-semibold">${notif.actor_name}</span>
${getNotificationText(notif)}
</p>
<p class="text-xs text-gray-500 mt-1">${timeAgo(notif.created_at)}</p>
</div>
${!notif.is_read ? '<span class="w-2 h-2 bg-blue-600 rounded-full"></span>' : ''}
</div>
</div>
`).join('');
}
// Get notification text based on type
function getNotificationText(notification) {
switch(notification.type) {
case 'like': return 'liked your post';
case 'comment': return 'commented on your post';
case 'share': return 'shared your post';
case 'friend_request': return 'sent you a friend request';
case 'friend_accept': return 'accepted your friend request';
case 'mention': return 'mentioned you in a post';
case 'message': return 'sent you a message';
case 'follow': return 'started following you';
case 'birthday': return 'has a birthday today';
default: return 'interacted with you';
}
}
// Mark notification as read
function markNotificationRead(notificationId) {
fetch('api/notifications.php?action=mark_read', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ notification_id: notificationId })
});
}
// Create post
function createPost() {
const content = document.getElementById('postContent').value;
const visibility = document.getElementById('postVisibility').value;
if (!content) {
alert('Please enter some content');
return;
}
showLoading();
const formData = new FormData();
formData.append('content', content);
formData.append('visibility', visibility);
// Add media files
const imageInput = document.getElementById('postImage');
if (imageInput.files.length > 0) {
for (let file of imageInput.files) {
formData.append('media[]', file);
}
}
fetch('api/posts.php?action=create', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
closeCreatePostModal();
loadHome();
} else {
alert('Failed to create post');
}
})
.finally(() => {
hideLoading();
});
}
// Like post
function likePost(postId) {
fetch('api/posts.php?action=like', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ post_id: postId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Refresh post
loadHome();
}
});
}
// Add comment
function addComment(postId) {
const input = document.getElementById(`comment-${postId}`);
const content = input.value.trim();
if (!content) return;
fetch('api/posts.php?action=comment', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
post_id: postId,
content: content
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
input.value = '';
loadComments(postId);
}
});
}
// Load comments
function loadComments(postId) {
fetch(`api/posts.php?action=comments&post_id=${postId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
displayComments(postId, data.comments);
}
});
}
// Display comments
function displayComments(postId, comments) {
const commentsDiv = document.getElementById(`comments-${postId}`);
const commentsList = document.getElementById(`comments-list-${postId}`);
commentsDiv.classList.remove('hidden');
commentsList.innerHTML = comments.map(comment => `
<div class="flex items-start space-x-2">
<img src="${comment.profile_pic || 'assets/images/default-avatar.png'}"
alt="${comment.username}"
class="w-6 h-6 rounded-full object-cover">
<div class="flex-1 bg-gray-100 rounded-lg px-3 py-2">
<p class="text-xs font-semibold">${comment.full_name}</p>
<p class="text-sm">${comment.content}</p>
<p class="text-xs text-gray-500 mt-1">${timeAgo(comment.created_at)}</p>
</div>
</div>
`).join('');
}
// Send friend request
function sendFriendRequest(userId) {
fetch('api/users.php?action=friend_request', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ user_id: userId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Friend request sent!');
}
});
}
// Search
function search() {
const query = document.getElementById('searchInput').value ||
document.getElementById('mobileSearchInput').value;
if (!query) return;
showLoading();
fetch(`api/search.php?q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
if (data.success) {
displaySearchResults(data.results, query);
}
})
.finally(() => {
hideLoading();
});
}
// Display search results
function displaySearchResults(results, query) {
const content = document.getElementById('contentArea');
content.innerHTML = `
<div class="bg-white rounded-lg shadow">
<div class="p-4 border-b">
<h3 class="text-lg font-semibold">Search Results for "${query}"</h3>
</div>
<!-- Users Results -->
${results.users && results.users.length > 0 ? `
<div class="p-4 border-b">
<h4 class="font-semibold mb-3">People</h4>
<div class="space-y-3">
${results.users.map(user => `
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<img src="${user.profile_pic || 'assets/images/default-avatar.png'}"
alt="${user.full_name}"
class="w-10 h-10 rounded-full object-cover cursor-pointer"
onclick="loadUserProfile(${user.id})">
<div>
<p class="font-medium cursor-pointer hover:underline" onclick="loadUserProfile(${user.id})">
${user.full_name}
</p>
<p class="text-sm text-gray-500">@${user.username}</p>
</div>
</div>
<button onclick="sendFriendRequest(${user.id})" class="text-blue-600 hover:text-blue-800">
<i class="fas fa-user-plus"></i>
</button>
</div>
`).join('')}
</div>
</div>
` : ''}
<!-- Posts Results -->
${results.posts && results.posts.length > 0 ? `
<div class="p-4">
<h4 class="font-semibold mb-3">Posts</h4>
<div class="space-y-4">
${results.posts.map(post => `
<div class="border rounded-lg p-3">
<div class="flex items-center space-x-2 mb-2">
<img src="${post.profile_pic || 'assets/images/default-avatar.png'}"
alt="${post.full_name}"
class="w-6 h-6 rounded-full object-cover">
<span class="text-sm font-medium">${post.full_name}</span>
</div>
<p class="text-sm">${post.content}</p>
</div>
`).join('')}
</div>
</div>
` : ''}
${(!results.users || results.users.length === 0) &&
(!results.posts || results.posts.length === 0) ? `
<div class="p-8 text-center text-gray-500">
No results found
</div>
` : ''}
</div>
`;
}
// Time ago helper
function timeAgo(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const seconds = Math.floor((now - date) / 1000);
if (seconds < 60) return 'just now';
if (seconds < 3600) return Math.floor(seconds / 60) + ' minutes ago';
if (seconds < 86400) return Math.floor(seconds / 3600) + ' hours ago';
if (seconds < 604800) return Math.floor(seconds / 86400) + ' days ago';
return date.toLocaleDateString();
}
// Initialize WebSocket for real-time updates
function initializeWebSocket() {
if (!currentUser) return;
// WebSocket implementation would go here
// For now, use polling
startPolling();
}
// Start polling for notifications
function startPolling() {
notificationInterval = setInterval(() => {
if (currentUser) {
fetch('api/notifications.php?action=count')
.then(response => response.json())
.then(data => {
if (data.success) {
updateNotificationBadge(data.count);
}
});
}
}, 30000); // Poll every 30 seconds
}
// Update notification badge
function updateNotificationBadge(count) {
const badge = document.getElementById('notificationBadge');
const mobileBadge = document.getElementById('mobileNotificationBadge');
if (count > 0) {
badge.textContent = count;
badge.classList.remove('hidden');
mobileBadge.textContent = count;
mobileBadge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
mobileBadge.classList.add('hidden');
}
}
// UI Helpers
function showLoading() {
document.getElementById('loadingOverlay').classList.remove('hidden');
}
function hideLoading() {
document.getElementById('loadingOverlay').classList.add('hidden');
}
function showLoginModal() {
document.getElementById('loginModal').classList.add('flex');
}
function showRegisterModal() {
closeModals();
document.getElementById('registerModal').classList.add('flex');
}
function closeModals() {
document.getElementById('loginModal').classList.remove('flex');
document.getElementById('registerModal').classList.remove('flex');
document.getElementById('createPostModal').classList.remove('flex');
}
function openCreatePostModal() {
document.getElementById('createPostModal').classList.add('flex');
}
function closeCreatePostModal() {
document.getElementById('createPostModal').classList.remove('flex');
document.getElementById('postContent').value = '';
document.getElementById('mediaPreview').innerHTML = '';
document.getElementById('mediaPreview').classList.add('hidden');
}
function toggleProfileDropdown() {
document.getElementById('profileMenu').classList.toggle('hidden');
}
function toggleMobileMenu() {
document.getElementById('mobileMenu').classList.toggle('hidden');
}
// Close dropdown when clicking outside
document.addEventListener('click', function(event) {
const dropdown = document.getElementById('profileMenu');
const button = event.target.closest('button');
if (!button && !dropdown.contains(event.target)) {
dropdown.classList.add('hidden');
}
});
</script>
</body>
</html>
PROJECT STRUCTURE
social-media-platform/ │ ├── index.html # Main application ├── config.php # Database configuration ├── helpers.php # Helper functions │ ├── classes/ │ ├── User.php # User management │ ├── Post.php # Post management │ ├── SocialRecommender.php # Recommendation engine │ ├── Message.php # Messaging system │ └── Notification.php # Notifications │ ├── api/ │ ├── auth.php # Authentication endpoints │ ├── users.php # User endpoints │ ├── posts.php # Post endpoints │ ├── recommendations.php # Recommendation endpoints │ ├── messages.php # Message endpoints │ ├── notifications.php # Notification endpoints │ └── search.php # Search endpoints │ ├── assets/ │ ├── images/ │ │ ├── default-avatar.png │ │ └── default-cover.jpg │ └── css/ │ └── style.css │ └── uploads/ ├── profiles/ ├── covers/ ├── posts/ └── messages/
FEATURES SUMMARY
✅ User Management
- Registration & Login
- Profile customization
- Profile/cover photos
- Privacy settings
- Account verification
✅ Social Features
- Friend requests & management
- Follow system
- Public/private accounts
- Block/report users
- Activity status
✅ Content Creation
- Text posts
- Photo/video uploads
- Post visibility options
- Hashtags & mentions
- Location tagging
✅ Engagement
- Likes with reactions
- Comments & replies
- Shares
- Saved posts
- Post moderation
✅ Recommendations
- Personalized feed
- Friend suggestions
- Trending content
- Hashtag discovery
- Similar content
✅ Messaging
- Real-time chat
- Media sharing
- Read receipts
- Conversation management
- Message history
✅ Notifications
- Real-time alerts
- Multiple notification types
- Read/unread tracking
- Push notifications ready
✅ Search
- User search
- Post search
- Hashtag search
- Advanced filters
- Search history
✅ Stories
- 24-hour content
- Story views tracking
- Media support
- Story archives
This complete social media platform includes all core features of modern social networks with a sophisticated content-based recommendation system that personalizes the user experience based on their interests, interactions, and behavior patterns.