1 <?php 2 require_once __DIR__ . '/../includes/bootstrap.php'; 3 4 $q = get('q'); 5 $type = get('type', 'topics'); // topics | users | posts 6 if (!in_array($type, ['topics','users','posts'])) $type = 'topics'; 7 8 $topicResults = []; 9 $postResults = []; 10 $userResults = []; 11 12 if (mb_strlen($q) >= 2) { 13 $like = '%' . $q . '%'; 14 15 if ($type === 'topics' || $type === 'posts') { 16 // Topics matching title 17 $topicResults = DB::rows(" 18 SELECT t.*, u.username, c.name AS cat_name, c.slug AS cat_slug, c.color AS cat_color, 19 NULL AS match_post_id 20 FROM topics t 21 JOIN users u ON u.id = t.user_id 22 JOIN categories c ON c.id = t.category_id 23 WHERE t.title LIKE ? 24 ORDER BY t.last_post_at DESC LIMIT 20 25 ", [$like]); 26 } 27 28 if ($type === 'posts') { 29 // Posts matching content β include post ID so we can anchor to it 30 $postResults = DB::rows(" 31 SELECT p.id AS post_id, p.content, p.post_num, p.created_at AS post_date, 32 t.id AS topic_id, t.title, t.slug, 33 u.username, u.avatar, 34 c.name AS cat_name, c.slug AS cat_slug, c.color AS cat_color 35 FROM posts p 36 JOIN topics t ON t.id = p.topic_id 37 JOIN users u ON u.id = p.user_id 38 JOIN categories c ON c.id = t.category_id 39 WHERE p.content LIKE ? AND p.deleted = 0 40 ORDER BY p.created_at DESC LIMIT 30 41 ", [$like]); 42 } 43 44 if ($type === 'users') { 45 $userResults = DB::rows(" 46 SELECT id, username, avatar, role, karma, post_count, bio, joined_at 47 FROM users 48 WHERE (username LIKE ? OR bio LIKE ?) AND suspended = 0 49 ORDER BY post_count DESC LIMIT 30 50 ", [$like, $like]); 51 } 52 } 53 54 $allCount = count($topicResults) + count($postResults) + count($userResults); 55 $PAGE_TITLE = 'Search'; 56 include __DIR__ . '/../views/partials/layout.php'; 57 ?> 58 <div style="max-width:800px"> 59 60 <!-- Search form with type tabs --> 61 <form method="GET" id="searchForm"> 62 <div style="display:flex;gap:10px;margin-bottom:16px"> 63 <div style="position:relative;flex:1;max-width:560px"> 64 <svg style="position:absolute;left:12px;top:50%;transform:translateY(-50%);color:var(--faint);pointer-events:none" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg> 65 <input type="text" name="q" value="<?= e($q) ?>" class="fi" 66 style="padding-left:34px" placeholder="Search topics, posts, usersβ¦" 67 autofocus autocomplete="off" id="mainSearchInput"> 68 </div> 69 <button type="submit" class="btn-primary">Search</button> 70 </div> 71 72 <!-- Type tabs --> 73 <div style="display:flex;gap:0;margin-bottom:22px;border-bottom:2px solid var(--border)"> 74 <?php foreach (['topics'=>'π Topics','posts'=>'π¬ Posts','users'=>'π₯ Users'] as $t => $lbl): ?> 75 <button type="submit" name="type" value="<?= $t ?>" 76 class="srch-tab <?= $type===$t?'active':'' ?>"><?= $lbl ?></button> 77 <?php endforeach; ?> 78 </div> 79 </form> 80 81 <?php if (mb_strlen($q) >= 2): ?> 82 <p class="search-info"> 83 <?= count($type==='posts'?$postResults:($type==='users'?$userResults:$topicResults)) ?> 84 result<?= count($type==='posts'?$postResults:($type==='users'?$userResults:$topicResults))!==1?'s':'' ?> 85 for "<strong><?= e($q) ?></strong>" 86 </p> 87 88 <?php if ($type === 'topics'): ?> 89 <?php if (empty($topicResults)): ?> 90 <div class="empty-state"><p>No topics found matching "<?= e($q) ?>".</p></div> 91 <?php else: ?> 92 <?php foreach ($topicResults as $t): ?> 93 <div class="topic-row"> 94 <div class="tr-body"> 95 <a href="<?= u('forum/topic.php?slug=' . urlencode($t['slug'])) ?>" class="tr-title"><?= e($t['title']) ?></a> 96 <div class="tr-meta"> 97 <a href="<?= u('forum/category.php?slug=' . urlencode($t['cat_slug'])) ?>" 98 class="cat-tag" style="--cc:<?= e($t['cat_color']) ?>"><?= e($t['cat_name']) ?></a> 99 by <a href="<?= u('users/profile.php?u=' . urlencode($t['username'])) ?>">@<?= e($t['username']) ?></a> 100 <span class="ago" data-ts="<?= e($t['created_at']) ?>"></span> 101 </div> 102 </div> 103 <div class="tr-counts"><span>π¬ <?= $t['reply_count'] ?></span><span>π <?= $t['views'] ?></span></div> 104 </div> 105 <?php endforeach; ?> 106 <?php endif; ?> 107 108 <?php elseif ($type === 'posts'): ?> 109 <?php if (empty($postResults)): ?> 110 <div class="empty-state"><p>No posts found matching "<?= e($q) ?>".</p></div> 111 <?php else: ?> 112 <?php foreach ($postResults as $p): ?> 113 <?php 114 // Build URL with post anchor so user lands on the exact post 115 $postUrl = u('forum/topic.php?slug=' . urlencode($p['slug']) . '&goto=' . $p['post_id']) . '#post-' . $p['post_id']; 116 // Highlight the search term in content snippet 117 $snippet = mb_substr($p['content'], 0, 200); 118 $pos = mb_stripos($p['content'], $q); 119 if ($pos !== false) { 120 $start = max(0, $pos - 60); 121 $snippet = ($start > 0 ? 'β¦' : '') . mb_substr($p['content'], $start, 200) . (mb_strlen($p['content']) > $start+200 ? 'β¦' : ''); 122 } 123 ?> 124 <div class="post-search-card"> 125 <div class="psc-head"> 126 <div style="display:flex;align-items:center;gap:8px;flex:1;min-width:0"> 127 <?php if ($p['avatar']): ?> 128 <img src="<?= e($p['avatar']) ?>" class="av-sm" alt=""> 129 <?php else: ?> 130 <span class="av-sm av-init"><?= strtoupper($p['username'][0]) ?></span> 131 <?php endif; ?> 132 <div style="min-width:0"> 133 <a href="<?= u('users/profile.php?u='.urlencode($p['username'])) ?>" style="font-weight:600;font-size:13px">@<?= e($p['username']) ?></a> 134 <span style="color:var(--faint);font-size:12px;margin-left:6px"> 135 in <a href="<?= u('forum/topic.php?slug='.urlencode($p['slug'])) ?>" style="color:var(--muted)"><?= e(mb_substr($p['title'],0,60)) ?><?= mb_strlen($p['title'])>60?'β¦':'' ?></a> 136 </span> 137 </div> 138 </div> 139 <div style="display:flex;align-items:center;gap:8px;flex-shrink:0"> 140 <span class="ago" data-ts="<?= e($p['post_date']) ?>" style="font-size:12px;color:var(--faint)"></span> 141 <a href="<?= e($postUrl) ?>" class="btn-ghost btn-sm">View Post β</a> 142 </div> 143 </div> 144 <div class="psc-body"><?= e($snippet) ?></div> 145 </div> 146 <?php endforeach; ?> 147 <?php endif; ?> 148 149 <?php elseif ($type === 'users'): ?> 150 <?php if (empty($userResults)): ?> 151 <div class="empty-state"><p>No users found matching "<?= e($q) ?>".</p></div> 152 <?php else: ?> 153 <div class="user-search-grid"> 154 <?php foreach ($userResults as $u): 155 $kt = karma_tier((int)$u['karma']); ?> 156 <a href="<?= u('users/profile.php?u='.urlencode($u['username'])) ?>" class="user-search-card"> 157 <div class="usc-av"> 158 <?php if ($u['avatar']): ?> 159 <img src="<?= e($u['avatar']) ?>" class="av-lg" alt=""> 160 <?php else: ?> 161 <span class="av-lg av-init"><?= strtoupper($u['username'][0]) ?></span> 162 <?php endif; ?> 163 </div> 164 <div class="usc-body"> 165 <div class="usc-name">@<?= e($u['username']) ?> <span class="role-tag role-<?= e($u['role']) ?>"><?= e($u['role']) ?></span></div> 166 <?php if ($u['bio']): ?><div class="usc-bio"><?= e(mb_substr($u['bio'],0,80)) ?></div><?php endif; ?> 167 <div class="usc-stats"> 168 <span><?= number_format($u['post_count']) ?> posts</span> 169 <span>Β·</span> 170 <span style="color:<?= e($kt['color']) ?>"><?= $kt['icon'] ?> <?= number_format((int)$u['karma']) ?> karma</span> 171 </div> 172 </div> 173 </a> 174 <?php endforeach; ?> 175 </div> 176 <?php endif; ?> 177 <?php endif; ?> 178 179 <?php elseif ($q): ?> 180 <div class="alert warn">Please enter at least 2 characters to search.</div> 181 <?php endif; ?> 182 </div> 183 184 <script> 185 // Auto-submit on tab change keeps query 186 document.querySelectorAll('.srch-tab').forEach(function(btn){ 187 btn.addEventListener('click',function(){ 188 document.getElementById('mainSearchInput').form.submit(); 189 }); 190 }); 191 </script> 192 <?php include __DIR__ . '/../views/partials/layout_end.php'; ?>