38873-vm/partners.php
Flatlogic Bot d515f936b1 v12
2026-02-28 17:30:02 +00:00

587 lines
25 KiB
PHP

<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
require_once __DIR__ . '/db/config.php';
$current_user_id = $_SESSION['user_id'];
$stmt = db()->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$current_user_id]);
$user = $stmt->fetch();
if (!$user || $user['role'] !== 'founder') {
header("Location: dashboard.php");
exit;
}
// Onboarding check
if (!$user['onboarding_completed']) {
header("Location: founder_onboarding.php");
exit;
}
// Skill-based Matching Helper
function calculateMatchScore($me, $them) {
$score = 0;
// 1. My preferred skills vs Their skills
$myNeeds = array_filter(array_map('trim', explode(',', strtolower($me['preferred_co_founder_skills'] ?? ''))));
$theirSkills = array_filter(array_map('trim', explode(',', strtolower($them['skills'] ?? ''))));
$skillMatches = array_intersect($myNeeds, $theirSkills);
$score += count($skillMatches) * 10;
// 2. Their preferred skills vs My skills
$theirNeeds = array_filter(array_map('trim', explode(',', strtolower($them['preferred_co_founder_skills'] ?? ''))));
$mySkills = array_filter(array_map('trim', explode(',', strtolower($me['skills'] ?? ''))));
$reciprocalMatches = array_intersect($theirNeeds, $mySkills);
$score += count($reciprocalMatches) * 10;
// 3. Industry overlap
$myIndustries = array_filter(array_map('trim', explode(',', strtolower($me['startup_industries'] ?? ''))));
$theirIndustries = array_filter(array_map('trim', explode(',', strtolower($them['startup_industries'] ?? ''))));
$industryMatches = array_intersect($myIndustries, $theirIndustries);
$score += count($industryMatches) * 5;
// 4. University match (bonus)
if (!empty($me['university']) && $me['university'] === $them['university']) {
$score += 5;
}
return [
'total' => $score,
'skillMatches' => array_values($skillMatches),
'industryMatches' => array_values($industryMatches)
];
}
// Handle Swipe Logic
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'swipe') {
header('Content-Type: application/json');
$swiped_id = (int)$_POST['swiped_id'];
$direction = $_POST['direction']; // 'like' or 'dislike'
if ($swiped_id > 0 && in_array($direction, ['like', 'dislike'])) {
$stmt = db()->prepare("INSERT IGNORE INTO swipes (swiper_id, swiped_id, direction) VALUES (?, ?, ?)");
$stmt->execute([$current_user_id, $swiped_id, $direction]);
$isMatch = false;
if ($direction === 'like') {
$stmt = db()->prepare("SELECT id FROM swipes WHERE swiper_id = ? AND swiped_id = ? AND direction = 'like'");
$stmt->execute([$swiped_id, $current_user_id]);
if ($stmt->fetch()) {
$isMatch = true;
$stmt = db()->prepare("INSERT IGNORE INTO matches (user1_id, user2_id) VALUES (?, ?)");
$u1 = min($current_user_id, $swiped_id);
$u2 = max($current_user_id, $swiped_id);
$stmt->execute([$u1, $u2]);
$stmt = db()->prepare("INSERT INTO notifications (user_id, content) VALUES (?, ?)");
$stmt->execute([$swiped_id, "You have a new match! Start a conversation now."]);
$stmt->execute([$current_user_id, "You have a new match! Start a conversation now."]);
}
}
echo json_encode(['status' => 'success', 'match' => $isMatch]);
exit;
}
echo json_encode(['status' => 'error']);
exit;
}
// Fetch matches
$stmt = db()->prepare("
SELECT u.*, m.created_at as matched_at FROM matches m
JOIN users u ON (m.user1_id = u.id OR m.user2_id = u.id)
WHERE (m.user1_id = ? OR m.user2_id = ?) AND u.id != ?
ORDER BY m.created_at DESC
");
$stmt->execute([$current_user_id, $current_user_id, $current_user_id]);
$matches = $stmt->fetchAll();
// Fetch swipe candidates with scoring
$stmt = db()->prepare("
SELECT * FROM users
WHERE role = 'founder'
AND id != ?
AND onboarding_completed = 1
AND looking_for_cofounder = 1
AND id NOT IN (SELECT swiped_id FROM swipes WHERE swiper_id = ?)
LIMIT 50
");
$stmt->execute([$current_user_id, $current_user_id]);
$allCandidates = $stmt->fetchAll();
foreach ($allCandidates as &$c) {
$c['match_data'] = calculateMatchScore($user, $c);
$c['score'] = $c['match_data']['total'];
}
usort($allCandidates, function($a, $b) { return $b['score'] <=> $a['score']; });
$swipeCandidates = array_slice($allCandidates, 0, 10);
// Fetch partners for Browse view
$search = $_GET['q'] ?? '';
$where = "role = 'founder' AND id != ? AND onboarding_completed = 1 AND id NOT IN (SELECT swiped_id FROM swipes WHERE swiper_id = ?)";
$params = [$current_user_id, $current_user_id];
if ($search) {
$where .= " AND (full_name LIKE ? OR skills LIKE ? OR university LIKE ? OR startup_industries LIKE ?)";
$params[] = "%$search%";
$params[] = "%$search%";
$params[] = "%$search%";
$params[] = "%$search%";
}
$stmt = db()->prepare("SELECT * FROM users WHERE $where LIMIT 40");
$stmt->execute($params);
$browseCandidates = $stmt->fetchAll();
foreach ($browseCandidates as &$c) {
$c['match_data'] = calculateMatchScore($user, $c);
$c['score'] = $c['match_data']['total'];
}
if (!$search) {
usort($browseCandidates, function($a, $b) { return $b['score'] <=> $a['score']; });
}
$platformName = defined('PLATFORM_NAME') ? PLATFORM_NAME : 'Gatsby';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Find Partners — <?= htmlspecialchars($platformName) ?></title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.page-header {
padding: 60px 0 40px;
text-align: center;
}
.page-header h1 {
font-size: 48px;
font-weight: 800;
margin-bottom: 12px;
background: var(--gradient-primary);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.page-header p {
color: var(--text-secondary);
font-size: 18px;
}
/* Matches Scroller */
.matches-section {
margin-bottom: 50px;
}
.matches-scroller {
display: flex;
gap: 20px;
overflow-x: auto;
padding: 15px 5px;
scrollbar-width: none;
}
.matches-scroller::-webkit-scrollbar { display: none; }
.match-card {
flex: 0 0 80px;
text-align: center;
cursor: pointer;
transition: transform 0.2s;
}
.match-card:hover { transform: scale(1.05); }
.match-avatar {
width: 80px;
height: 80px;
border-radius: 24px;
background: var(--surface-color);
border: 2px solid var(--accent-blue);
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
font-weight: 700;
color: #fff;
margin-bottom: 8px;
box-shadow: 0 10px 20px rgba(0, 242, 255, 0.2);
position: relative;
}
.match-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 22px; }
.match-name { font-size: 12px; font-weight: 600; color: var(--text-primary); }
/* Tabs */
.tabs {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 40px;
}
.tab-btn {
padding: 12px 24px;
border-radius: 50px;
background: var(--surface-color);
border: 1px solid var(--border-color);
color: var(--text-secondary);
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.tab-btn.active {
background: var(--gradient-primary);
color: #fff;
border-color: transparent;
box-shadow: 0 10px 20px rgba(0, 122, 255, 0.3);
}
/* Swipe UI */
.swipe-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
}
.card-stack {
position: relative;
width: 100%;
max-width: 400px;
height: 550px;
}
.swipe-card {
position: absolute;
width: 100%;
height: 100%;
background: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: 32px;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0,0,0,0.4);
transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.5s ease;
cursor: grab;
display: flex;
flex-direction: column;
}
.card-header-img {
height: 280px;
background: var(--gradient-primary);
display: flex;
align-items: center;
justify-content: center;
font-size: 100px;
color: rgba(255,255,255,0.2);
position: relative;
}
.match-badge {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(10px);
padding: 8px 16px;
border-radius: 50px;
color: var(--accent-blue);
font-weight: 700;
font-size: 14px;
border: 1px solid var(--accent-blue);
display: flex;
align-items: center;
gap: 8px;
}
.card-details { padding: 25px; flex-grow: 1; display: flex; flex-direction: column; }
.card-title { font-size: 26px; font-weight: 800; margin-bottom: 5px; }
.card-subtitle { font-size: 14px; color: var(--accent-blue); font-weight: 600; margin-bottom: 15px; }
.card-bio { font-size: 14px; color: var(--text-secondary); line-height: 1.5; margin-bottom: 15px; }
.card-tags { display: flex; flex-wrap: wrap; gap: 8px; }
.card-tag { font-size: 11px; padding: 5px 12px; background: rgba(255,255,255,0.05); border-radius: 20px; border: 1px solid var(--border-color); }
.card-tag.highlight { border-color: var(--accent-blue); color: var(--accent-blue); background: rgba(0, 242, 255, 0.05); }
.swipe-actions { display: flex; gap: 20px; margin-top: 30px; }
.action-btn {
width: 70px; height: 70px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
font-size: 28px; cursor: pointer; border: none; transition: transform 0.2s; box-shadow: 0 10px 20px rgba(0,0,0,0.2);
}
.action-btn:hover { transform: scale(1.1); }
.btn-dislike { background: #1a1a24; color: #ff3b30; border: 1px solid rgba(255, 39, 48, 0.2); }
.btn-like { background: var(--gradient-primary); color: #fff; }
/* Browse UI */
.browse-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 25px;
}
.candidate-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 24px;
padding: 25px;
transition: transform 0.3s;
cursor: pointer;
position: relative;
}
.candidate-card:hover { transform: translateY(-5px); border-color: var(--accent-blue); }
.candidate-header { display: flex; gap: 15px; align-items: center; margin-bottom: 20px; }
.candidate-avatar { width: 60px; height: 60px; border-radius: 18px; background: var(--gradient-primary); display: flex; align-items: center; justify-content: center; font-weight: 700; color: #fff; font-size: 20px; }
.score-indicator {
position: absolute;
top: 25px;
right: 25px;
font-size: 11px;
font-weight: 700;
color: var(--accent-blue);
text-transform: uppercase;
}
/* Match Modal */
#match-modal {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(10, 10, 15, 0.95); z-index: 2000; display: none;
align-items: center; justify-content: center; text-align: center;
backdrop-filter: blur(20px);
}
.match-popup { max-width: 400px; padding: 40px; }
.match-title { font-size: 56px; font-weight: 900; background: var(--gradient-primary); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 20px; }
</style>
</head>
<body>
<header>
<div class="container" style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
<div class="logo"><?= htmlspecialchars($platformName) ?></div>
<nav class="nav-links">
<a href="startups.php">My Startups</a>
<a href="partners.php" class="active">Find Partners</a>
<a href="discover.php">Discovery Hub</a>
<a href="messages.php">Messages</a>
<a href="notifications.php">Notifications</a>
</nav>
<div style="display: flex; align-items: center; gap: 15px;">
<a href="dashboard.php" style="color: var(--text-secondary);"><i class="fas fa-th-large"></i></a>
<a href="logout.php" class="btn btn-secondary" style="padding: 8px 16px; font-size: 13px;">Log Out</a>
</div>
</div>
</header>
<main class="container">
<div class="hero-bg">
<div class="hero-blob" style="top: 10%; left: -10%;"></div>
<div class="hero-blob" style="top: 40%; right: -10%; width: 400px; height: 400px; background: radial-gradient(circle, rgba(138, 43, 226, 0.1) 0%, rgba(0, 242, 255, 0.1) 100%);"></div>
</div>
<div class="page-header">
<h1>Partnership Discovery</h1>
<p>Find your next co-founder, advisor, or early collaborator.</p>
</div>
<?php if (!empty($matches)): ?>
<section class="matches-section">
<h3 style="margin-bottom: 20px; font-size: 18px; color: var(--text-secondary);">Your Recent Matches</h3>
<div class="matches-scroller">
<?php foreach ($matches as $match): ?>
<div class="match-card" onclick="location.href='messages.php?user_id=<?= $match['id'] ?>'">
<div class="match-avatar">
<?php if ($match['profile_photo']): ?>
<img src="<?= htmlspecialchars($match['profile_photo']) ?>">
<?php else: ?>
<?= substr($match['full_name'], 0, 1) ?>
<?php endif; ?>
</div>
<div class="match-name"><?= htmlspecialchars(explode(' ', $match['full_name'])[0]) ?></div>
</div>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
<div class="tabs">
<button class="tab-btn active" id="tab-swipe" onclick="setTab('swipe')">Swipe Mode</button>
<button class="tab-btn" id="tab-browse" onclick="setTab('browse')">Browse Mode</button>
</div>
<div id="swipe-view" class="swipe-container">
<div class="card-stack" id="card-stack">
<?php if (empty($swipeCandidates)): ?>
<div style="text-align: center; color: var(--text-secondary); padding: 100px 0;">
<i class="fas fa-search" style="font-size: 48px; margin-bottom: 20px; opacity: 0.3;"></i>
<h3>Out of candidates!</h3>
<p>Try switching to Browse Mode or check back later.</p>
</div>
<?php else: ?>
<?php foreach (array_reverse($swipeCandidates) as $c): ?>
<div class="swipe-card" data-id="<?= $c['id'] ?>">
<div class="card-header-img">
<?php if ($c['score'] >= 10): ?>
<div class="match-badge"><i class="fas fa-bolt"></i> <?= $c['score'] ?>% Compatible</div>
<?php endif; ?>
<?php if ($c['profile_photo']): ?>
<img src="<?= htmlspecialchars($c['profile_photo']) ?>" style="width: 100%; height: 100%; object-fit: cover;">
<?php else: ?>
<i class="fas fa-user-astronaut"></i>
<?php endif; ?>
</div>
<div class="card-details">
<div class="card-title"><?= htmlspecialchars($c['full_name']) ?></div>
<div class="card-subtitle"><?= htmlspecialchars($c['university']) ?> • <?= htmlspecialchars($c['degree_program']) ?></div>
<p class="card-bio"><?= htmlspecialchars($c['bio'] ?: 'A visionary founder looking for a like-minded partner.') ?></p>
<div class="card-tags">
<?php
$skills = explode(',', $c['skills']);
$myNeeds = array_map('trim', explode(',', strtolower($user['preferred_co_founder_skills'] ?? '')));
foreach (array_slice($skills, 0, 4) as $skill):
if(empty(trim($skill))) continue;
$isNeeded = in_array(trim(strtolower($skill)), $myNeeds);
?>
<span class="card-tag <?= $isNeeded ? 'highlight' : '' ?>">
<?= $isNeeded ? '<i class="fas fa-check"></i> ' : '' ?>
<?= htmlspecialchars(trim($skill)) ?>
</span>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php if (!empty($swipeCandidates)): ?>
<div class="swipe-actions">
<button class="action-btn btn-dislike" onclick="handleSwipe('dislike')"><i class="fas fa-times"></i></button>
<button class="action-btn btn-like" onclick="handleSwipe('like')"><i class="fas fa-heart"></i></button>
</div>
<?php endif; ?>
</div>
<div id="browse-view" style="display: none;">
<div style="margin-bottom: 30px;">
<form method="GET" style="display: flex; gap: 10px; max-width: 600px; margin: 0 auto;">
<input type="text" name="q" value="<?= htmlspecialchars($search) ?>" placeholder="Search by name, skill, university..." class="form-control" style="flex: 1; background: var(--surface-color); border: 1px solid var(--border-color); color: #fff; padding: 12px 20px; border-radius: 12px;">
<button type="submit" class="btn btn-primary">Search</button>
</form>
</div>
<div class="browse-container">
<?php foreach ($browseCandidates as $c): ?>
<div class="candidate-card" onclick="location.href='messages.php?user_id=<?= $c['id'] ?>'">
<?php if ($c['score'] >= 10): ?>
<div class="score-indicator"><i class="fas fa-star"></i> Top Match</div>
<?php endif; ?>
<div class="candidate-header">
<div class="candidate-avatar">
<?php if ($c['profile_photo']): ?>
<img src="<?= htmlspecialchars($c['profile_photo']) ?>" style="width: 100%; height: 100%; object-fit: cover; border-radius: 16px;">
<?php else: ?>
<?= substr($c['full_name'], 0, 1) ?>
<?php endif; ?>
</div>
<div>
<div style="font-weight: 700; font-size: 18px;"><?= htmlspecialchars($c['full_name']) ?></div>
<div style="font-size: 13px; color: var(--accent-blue);"><?= htmlspecialchars($c['university']) ?></div>
</div>
</div>
<p style="font-size: 14px; color: var(--text-secondary); margin-bottom: 15px; line-height: 1.5; height: 42px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;">
<?= htmlspecialchars($c['bio'] ?: 'Looking for a co-founder to build something great.') ?>
</p>
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
<?php
$skills = explode(',', $c['skills']);
$myNeeds = array_map('trim', explode(',', strtolower($user['preferred_co_founder_skills'] ?? '')));
foreach (array_slice($skills, 0, 3) as $skill):
if(empty(trim($skill))) continue;
$isNeeded = in_array(trim(strtolower($skill)), $myNeeds);
?>
<span style="font-size: 10px; padding: 3px 10px; background: <?= $isNeeded ? 'rgba(0, 242, 255, 0.1)' : 'rgba(255,255,255,0.05)' ?>; border-radius: 20px; color: <?= $isNeeded ? 'var(--accent-blue)' : 'var(--text-secondary)' ?>; border: 1px solid <?= $isNeeded ? 'var(--accent-blue)' : 'transparent' ?>;">
<?= htmlspecialchars(trim($skill)) ?>
</span>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</main>
<div id="match-modal">
<div class="match-popup">
<h1 class="match-title">It's a Match!</h1>
<p style="color: var(--text-secondary); margin-bottom: 30px; font-size: 18px;">You and this founder both swiped right. Ready to build the next big thing?</p>
<button class="btn btn-primary" style="width: 100%; margin-bottom: 15px; padding: 15px;" onclick="location.href='messages.php'">Start a Conversation</button>
<button class="btn btn-secondary" style="width: 100%; padding: 15px;" onclick="closeMatch()">Keep Exploring</button>
</div>
</div>
<script>
function setTab(tab) {
document.getElementById('swipe-view').style.display = tab === 'swipe' ? 'flex' : 'none';
document.getElementById('browse-view').style.display = tab === 'browse' ? 'block' : 'none';
document.getElementById('tab-swipe').classList.toggle('active', tab === 'swipe');
document.getElementById('tab-browse').classList.toggle('active', tab === 'browse');
}
function handleSwipe(direction) {
const stack = document.getElementById('card-stack');
const cards = stack.querySelectorAll('.swipe-card');
if (cards.length === 0) return;
const topCard = cards[cards.length - 1];
const swipedId = topCard.getAttribute('data-id');
// Animation
const rotate = direction === 'like' ? 30 : -30;
const x = direction === 'like' ? 1000 : -1000;
topCard.style.transform = `translateX(${x}px) rotate(${rotate}deg)`;
topCard.style.opacity = '0';
// POST request
const fd = new FormData();
fd.append('action', 'swipe');
fd.append('swiped_id', swipedId);
fd.append('direction', direction);
fetch('partners.php', { method: 'POST', body: fd })
.then(r => r.json())
.then(data => {
if (data.match) {
document.getElementById('match-modal').style.display = 'flex';
}
setTimeout(() => {
topCard.remove();
if (stack.querySelectorAll('.swipe-card').length === 0) {
location.reload();
}
}, 500);
});
}
function closeMatch() {
document.getElementById('match-modal').style.display = 'none';
}
</script>
<style>
header {
background: rgba(10, 10, 15, 0.8);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border-color);
padding: 15px 0;
position: sticky;
top: 0;
z-index: 1000;
}
.nav-links a {
color: var(--text-secondary);
text-decoration: none;
margin: 0 15px;
font-size: 14px;
font-weight: 500;
transition: color 0.2s;
}
.nav-links a:hover, .nav-links a.active {
color: #fff;
}
.form-control:focus {
outline: none;
border-color: var(--accent-blue);
}
</style>
</body>
</html>