Forums
The great place to discuss topics with other users
User LeaderBoard + Badges
'I leave here for free my leaderboard + Badge
class-user.php
// LEADERBOARD public function getLeaderboardData($user_id) { global $db; // Fetch top 100 users with activity stats $result = $db->query(" SELECT user_id, user_name, user_points, user_registered, RANK() OVER (ORDER BY user_points DESC) AS user_rank FROM users WHERE user_points > 0 ORDER BY user_points DESC LIMIT 100 ") or _error(SQL_ERROR_THROWEN); $leaderboard = []; while ($row = $result->fetch_assoc()) { $row['badges'] = $this->assignBadges($row); $leaderboard[] = $row; } // Identify current user and rank $user_index = array_search($user_id, array_column($leaderboard, 'user_id')); $current_user = null; $next_user = null; if ($user_index !== false) { $current_user = $leaderboard[$user_index]; $next_user = $user_index > 0 ? $leaderboard[$user_index - 1] : null; } else { // User is not in top 100 $user_row = $db->query(" SELECT user_id, user_name, user_points, user_registered, (SELECT COUNT(*) + 1 FROM users WHERE user_points > u.user_points) AS user_rank FROM users u WHERE user_id = '{$user_id}' LIMIT 1 ") or _error(SQL_ERROR_THROWEN); if ($user_row->num_rows > 0) { $current_user = $user_row->fetch_assoc(); $current_user['badges'] = $this->assignBadges($current_user); // Next user $next_row = $db->query(" SELECT user_name, user_points FROM users WHERE user_points > '{$current_user['user_points']}' ORDER BY user_points ASC LIMIT 1 ") or _error(SQL_ERROR_THROWEN); if ($next_row->num_rows > 0) { $next_user = $next_row->fetch_assoc(); } } } return [ 'top_users' => $leaderboard, 'current_user' => $current_user, 'next_user' => $next_user ];}public static function getAllBadges(){ $site_launch = '2025-05-24'; return [ [ 'key' => 'veteran', 'label' => 'Veteran', 'icon' => '/content/uploads/leaderboard/veteran.svg', 'description' => 'Registered for over 1 year.', 'requirement' => 'Account age ≥ 1 year', 'unlocked' => function($user) { $years = (strtotime('now') - strtotime($user['user_registered'])) / (60 * 60 * 24 * 365); return $years >= 1; }, 'level' => function($user) { $years = (strtotime('now') - strtotime($user['user_registered'])) / (60 * 60 * 24 * 365); if ($years >= 3) return 'gold'; if ($years >= 2) return 'silver'; if ($years >= 1) return 'bronze'; return null; } ], [ 'key' => 'top_points', 'label' => 'Active Member', 'icon' => '/content/uploads/leaderboard/contributor.svg', 'description' => 'Earned at least 1000 points.', 'requirement' => 'Points ≥ 1000', 'unlocked' => function($user) { return $user['user_points'] >= 1000; }, 'level' => function($user) { if ($user['user_points'] >= 5000) return 'gold'; if ($user['user_points'] >= 2500) return 'silver'; if ($user['user_points'] >= 1000) return 'bronze'; return null; } ], [ 'key' => 'early_supporter', 'label' => 'Supporter', 'icon' => '/content/uploads/leaderboard/early.svg', 'description' => 'Joined in the first 3 months.', 'requirement' => 'Registered before 2025-08-24', 'unlocked' => function($user) use ($site_launch) { return strtotime($user['user_registered']) <= strtotime("$site_launch +3 months"); }, 'level' => function($user) use ($site_launch) { $joined = strtotime($user['user_registered']); if ($joined <= strtotime("$site_launch +1 month")) return 'gold'; if ($joined <= strtotime("$site_launch +2 months")) return 'silver'; if ($joined <= strtotime("$site_launch +3 months")) return 'bronze'; return null; } ] ];}public static function assignBadges($user){ $all = self::getAllBadges(); $user_badges = []; foreach ($all as $badge) { $badge_levels = []; if ($badge['key'] === 'top_points') { $badge_levels = [ 'bronze' => [ 'requirement' => 1000, 'unlocked' => $user['user_points'] >= 1000, 'progress' => isset($user['user_points']) ? (int)$user['user_points'] : 0, ], 'silver' => [ 'requirement' => 2500, 'unlocked' => $user['user_points'] >= 2500, 'progress' => isset($user['user_points']) ? (int)$user['user_points'] : 0, ], 'gold' => [ 'requirement' => 5000, 'unlocked' => $user['user_points'] >= 5000, 'progress' => isset($user['user_points']) ? (int)$user['user_points'] : 0, ], ]; } if ($badge['key'] === 'veteran') { $years = (time() - strtotime($user['user_registered'])) / (60 * 60 * 24 * 365); $badge_levels = [ 'bronze' => [ 'requirement' => 1, 'unlocked' => $years >= 1, 'progress' => $years, ], 'silver' => [ 'requirement' => 2, 'unlocked' => $years >= 2, 'progress' => $years, ], 'gold' => [ 'requirement' => 3, 'unlocked' => $years >= 3, 'progress' => $years, ], ]; } if ($badge['key'] === 'early_supporter') { $registered = strtotime($user['user_registered']); $badge_levels = [ 'bronze' => [ 'requirement' => strtotime("2025-08-24"), 'progress' => $registered, 'unlocked' => $registered <= strtotime("2025-08-24"), ], 'silver' => [ 'requirement' => strtotime("2025-07-24"), 'progress' => $registered, 'unlocked' => $registered <= strtotime("2025-07-24"), ], 'gold' => [ 'requirement' => strtotime("2025-06-24"), 'progress' => $registered, 'unlocked' => $registered <= strtotime("2025-06-24"), ], ]; } $unlocked_level = null; foreach (['gold', 'silver', 'bronze'] as $lvl) { if (!empty($badge_levels[$lvl]['unlocked'])) { $unlocked_level = $lvl; break; } } $user_badges[] = [ 'key' => $badge['key'], 'label' => $badge['label'], 'icon' => $badge['icon'], 'description' => $badge['description'], 'requirement' => $badge['requirement'], 'unlocked' => call_user_func($badge['unlocked'], $user), 'unlocked_level' => $unlocked_level, 'levels' => $badge_levels ]; } return $user_badges; }leaderboard.php
<?php/** * notifications * * @package Sngine * @author Zamblek */// fetch bootloaderrequire('bootloader.php');// user accessuser_access();// page headerpage_header(__("TOP 100 ACTIVE MEMBERS"));try { $leaderboard_data = $user->getLeaderboardData($user->_data['user_id']); $smarty->assign('leaderboard', $leaderboard_data); $user_badges = User::assignBadges($user->_data); $smarty->assign('user_badges', $user_badges);} catch (Exception $e) { _error(__("Error"), $e->getMessage());}// page footerpage_footer('leaderboard');<h4 class="mt-3 mb-3 text-center">{__("TOP 100 ACTIVE MEMBERS")} (<a href="{$system['system_url']}/settings/points">INFO</a>)</h4>
<div class="badge-gallery"> {foreach from=$user_badges item=badge} <div class="badgepp badge-container"> {if $badge.unlocked_level} <img src="{$system['system_url']}/content/uploads/leaderboard/{$badge.key}_{$badge.unlocked_level}.png" class="badge-iconpp" width="40" height="40" /> {else} <h6 class="text-uppercase text-muted">{__("No badge unlocked")}</h6> {/if}
<div class="badge-info"> <h5 class="mb-1">{__($badge.label)}</h5> <p class="text-muted mb-2">{__($badge.description)}</p>
{foreach from=$badge.levels key=level item=level_data} <div class="badge-level-entry badge-level-{$level} {if $level_data.unlocked}unlocked{else}locked{/if}"> <img src="{$system['system_url']}/content/uploads/leaderboard/{$badge.key}_{$level}.png" alt="{$level}" class="me-1" width="20" height="20" /> <strong class="text-capitalize">{$level}</strong> –
{if $badge.key == 'veteran'} <strong>{$level_data.requirement} {__("years")}</strong> {if !$level_data.unlocked} – <strong class="text-danger">{__("Needs")} {math equation="x - y" x=$level_data.requirement y=$level_data.progress format="%.1f"} {__("years")}</strong> {/if} {elseif $badge.key == 'early_supporter'} {__("Joined before")} {$level_data.requirement|date_format:"%e %B, %Y"} {if !$level_data.unlocked} – <span class="text-danger">{__("Too late")}</span> {/if}
{else} (<strong>{$level_data.requirement} {__("points")}</strong>) {if !$level_data.unlocked} – <strong class="text-danger">{__("Needs")} {math equation="x - y" x=$level_data.requirement y=$level_data.progress|default:0 format="%d"}</strong> {/if} {/if} </div> {/foreach} </div> </div> {/foreach} </div>
{if $leaderboard.current_user} <div class="alert alert-info mt-4"> {__("You are ranked")} <strong>#{$leaderboard.current_user.user_rank}</strong> {__("with")} <strong>{$leaderboard.current_user.user_points}</strong> {__("points")}. {if $leaderboard.next_user} <br> {__("Next rank")}: <strong>{$leaderboard.next_user.user_name}</strong> {__("with")} <strong>{$leaderboard.next_user.user_points}</strong> {__("points")}. <br> {__("Needed to pass")}: <strong>{$leaderboard.next_user.user_points - $leaderboard.current_user.user_points + 1}</strong> {__("points")} {/if} </div> {/if}
<div class="leaderboardp">
{foreach $leaderboard.top_users as $user} <a href="{$system['system_url']}/{$user.user_name}"> <div class="user-rowp {if $user.user_id == $leaderboard.current_user.user_id}highlightp{/if}"> <div class="rankp"> {if $user.user_rank == 1}<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" viewBox="0 0 32 32"><g fill="none"><path fill="#0074ba" d="m18.768 11.51l-5.22-8.58c-.34-.58-.95-.93-1.62-.93h-6.59c-1.45 0-2.36 1.56-1.65 2.82a16.7 16.7 0 0 0 5.43 5.78c.76.59 1.7.91 2.67.91z"/><path fill="#00a6ed" d="M26.658 2h-6.59c-.67 0-1.28.35-1.62.93l-5.22 8.58h6.99c.97 0 1.9-.32 2.67-.91c2.25-1.46 4.11-3.44 5.43-5.78c.7-1.26-.21-2.82-1.66-2.82"/><path fill="#ffb02e" d="M15.99 30c5.545 0 10.04-4.607 10.04-10.29S21.535 9.42 15.99 9.42S5.95 14.027 5.95 19.71S10.445 30 15.99 30"/><path fill="#6d4534" d="M14.076 16.041a1 1 0 0 1 1-1H16a1 1 0 0 1 1 1V23a1 1 0 1 1-2 0v-5.962a1 1 0 0 1-.924-.997"/><path fill="#fcd53f" d="M16 28.76c-2.36 0-4.58-.94-6.24-2.65a9.1 9.1 0 0 1-2.59-6.4c0-2.42.92-4.69 2.59-6.4a8.69 8.69 0 0 1 12.49 0c3.44 3.53 3.44 9.27 0 12.8c-1.68 1.71-3.9 2.65-6.25 2.65m-.01-16.87c-1.95 0-3.91.76-5.39 2.29a7.87 7.87 0 0 0-2.23 5.53c0 2.09.79 4.05 2.23 5.53a7.48 7.48 0 0 0 5.39 2.29c2.04 0 3.95-.81 5.39-2.29c2.97-3.05 2.97-8.01 0-11.06a7.46 7.46 0 0 0-5.39-2.29"/></g></svg> {elseif $user.user_rank == 2}<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" viewBox="0 0 32 32"><g fill="none"><path fill="#0074ba" d="m18.768 11.51l-5.22-8.58c-.34-.58-.95-.93-1.62-.93h-6.59c-1.45 0-2.36 1.56-1.65 2.82a16.7 16.7 0 0 0 5.43 5.78c.76.59 1.7.91 2.67.91z"/><path fill="#00a6ed" d="M26.658 2h-6.59c-.67 0-1.28.35-1.62.93l-5.22 8.58h6.99c.97 0 1.9-.32 2.67-.91c2.25-1.46 4.11-3.44 5.43-5.78c.7-1.26-.21-2.82-1.66-2.82"/><path fill="#bebebe" d="M15.96 30.001c5.545 0 10.04-4.607 10.04-10.29s-4.495-10.29-10.04-10.29s-10.04 4.607-10.04 10.29s4.495 10.29 10.04 10.29"/><path fill="#e6e6e6" d="M15.96 28.761c-2.36 0-4.58-.94-6.24-2.65a9.1 9.1 0 0 1-2.59-6.4c0-2.42.92-4.69 2.59-6.4a8.69 8.69 0 0 1 12.49 0c3.44 3.53 3.44 9.27 0 12.8c-1.68 1.71-3.9 2.65-6.25 2.65m-.01-16.87c-1.95 0-3.91.76-5.39 2.29a7.87 7.87 0 0 0-2.23 5.53c0 2.09.79 4.05 2.23 5.53a7.48 7.48 0 0 0 5.39 2.29c2.04 0 3.95-.81 5.39-2.29c2.97-3.05 2.97-8.01 0-11.06a7.46 7.46 0 0 0-5.39-2.29"/><path fill="#636363" d="M17.838 23.95h-3.97a1 1 0 0 1-.91-.58a1 1 0 0 1 .13-1.07l3.3-4.05c.26-.32.14-.66.1-.76a.64.64 0 0 0-.58-.4h-.05c-.32 0-.61.15-.8.41c-.32.45-.95.56-1.41.24a1.01 1.01 0 0 1-.24-1.41c.56-.79 1.47-1.26 2.44-1.26h.13c1.02.05 1.91.66 2.33 1.59c.43.96.29 2.05-.37 2.86l-1.95 2.4h1.84c.56 0 1.01.45 1.01 1.01s-.44 1.02-1 1.02"/></g></svg> {elseif $user.user_rank == 3}<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" viewBox="0 0 32 32"><g fill="none"><path fill="#0074ba" d="m18.768 11.51l-5.22-8.58c-.34-.58-.95-.93-1.62-.93h-6.59c-1.45 0-2.36 1.56-1.65 2.82a16.7 16.7 0 0 0 5.43 5.78c.76.59 1.7.91 2.67.91z"/><path fill="#00a6ed" d="M26.658 2h-6.59c-.67 0-1.28.35-1.62.93l-5.22 8.58h6.99c.97 0 1.9-.32 2.67-.91c2.25-1.46 4.11-3.44 5.43-5.78c.7-1.26-.21-2.82-1.66-2.82"/><path fill="#d3883e" d="M15.99 30c5.545 0 10.04-4.607 10.04-10.29S21.535 9.42 15.99 9.42S5.95 14.027 5.95 19.71S10.445 30 15.99 30"/><path fill="#f3ad61" d="M16 28.76c-2.36 0-4.58-.94-6.24-2.65a9.1 9.1 0 0 1-2.59-6.4c0-2.42.92-4.69 2.59-6.4a8.69 8.69 0 0 1 12.49 0c3.44 3.53 3.44 9.27 0 12.8c-1.68 1.71-3.9 2.65-6.25 2.65m-.01-16.87c-1.95 0-3.91.76-5.39 2.29a7.87 7.87 0 0 0-2.23 5.53c0 2.09.79 4.05 2.23 5.53a7.48 7.48 0 0 0 5.39 2.29c2.04 0 3.95-.81 5.39-2.29c2.97-3.05 2.97-8.01 0-11.06a7.46 7.46 0 0 0-5.39-2.29"/><path fill="#402a32" d="m17.402 18.275l1.048-1.81a.98.98 0 0 0 0-.977a.97.97 0 0 0-.844-.488h-3.092a.977.977 0 0 0 0 1.953h1.403l-.752 1.311a.96.96 0 0 0-.214.6c0 .54.438.977.977.977c.61 0 1.108.498 1.108 1.108s-.498 1.109-1.108 1.109c-.468 0-.885-.295-1.038-.733a.975.975 0 0 0-1.24-.59a.966.966 0 0 0-.59 1.241A3.06 3.06 0 0 0 15.928 24a3.053 3.053 0 0 0 3.05-3.05a3.07 3.07 0 0 0-1.576-2.675"/></g></svg> {elseif $user.user_rank >= 4} {$user.user_rank} {/if} </div> <div class="user-infop"> <div class="usernamep"> {if $user.user_id == $leaderboard.current_user.user_id} <span class="badge bg-primary ms-1">{__("YOU ARE HERE")}</span> {else} {$user.user_name} {/if} </div> <div class="badgesp"> {foreach $user.badges as $badge} {if $badge.unlocked_level} <img src="{$system['system_url']}/content/uploads/leaderboard/{$badge.key}_{$badge.unlocked_level}.png" class="badge-iconpp" width="20" height="20" /> {/if} {/foreach} </div> </div> <div class="pointsp">{$user.user_points}</div> </div> </a> {/foreach}
</div>
style.css
/* LEADERBOARD PUNCTE */.leaderboardp { max-width: 800px; margin: 0 auto; border-radius: 10px; overflow: hidden;}.rankp,.pointsp { display: flex; flex-direction: column; justify-content: center;}.user-rowp { box-sizing: border-box; display: flex; justify-content: space-between; align-items: center; /* <- This is the key change */ background: #fff; padding: 15px 20px; border-bottom: 1px solid #ddd; transition: background 0.3s;}body.night-mode .user-rowp { background: #212121; border-bottom: 1px solid #485259;}.user-rowp.highlightp { position: relative; background: #ffe9f0;}body.night-mode .user-rowp.highlightp { background: #1D1D1D;}.user-rowp.highlightp::before { content: ""; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background-color: #ff3399;}.rankp { font-weight: bold; min-width: 40px; color: #1D1D1D; font-size: 1.3rem;}body.night-mode .rankp { color: #f8f8f8;}.user-infop { flex-grow: 1; padding: 0 20px;}.usernamep { font-size: 1.1em; font-weight: bold; margin-bottom: 5px;}.badgesp { display: flex; flex-wrap: wrap; gap: 5px;}.badgep { font-size: 0.8em; padding: 3px 8px; border-radius: 12px; background: #ccc; color: #fff;}.badgep.unlockedp { background-color: #28a745;}.badgep.lockedp { background-color: #6c757d;}.pointsp { min-width: 100px; text-align: right; font-weight: bold; color: #28a745; font-size: 1.4rem;}.badge.lockedpp { opacity: 0.5;}.badgepp { padding: 5px; border-radius: 8px; display: flex; flex-direction: column; align-items: start; margin-bottom: 10px;}.badge-iconpp { display: inline-block; vertical-align: middle; width: 32px; height: 32px;}.locked { background-color: #e9ecef;}.badge-container { border: 1px solid #ddd; border-radius: 8px; padding: 15px; margin-bottom: 20px; background: #fff; display: flex; gap: 15px;}body.night-mode .badge-container { background: #212121; border: 1px solid #485259;}.badge-iconpp { flex-shrink: 0;}.badge-info { flex-grow: 1;}.badge-level-entry { margin-top: 5px;}.badge-level-entry.unlocked { color: #28a745; font-weight: bold;}.badge-level-entry.locked { color: #6c757d;}
I leave here for free my leaderboard + Badge
class-user.php
// LEADERBOARD public function getLeaderboardData($user_id) { global $db; // Fetch top 100 users with activity stats $result = $db->query(" SELECT user_id, user_name, user_points, user_registered, RANK() OVER (ORDER BY user_points DESC) AS user_rank FROM users WHERE user_points > 0 ORDER BY user_points DESC LIMIT 100 ") or _error(SQL_ERROR_THROWEN); $leaderboard = []; while ($row = $result->fetch_assoc()) { $row['badges'] = $this->assignBadges($row); $leaderboard[] = $row; } // Identify current user and rank $user_index = array_search($user_id, array_column($leaderboard, 'user_id')); $current_user = null; $next_user = null; if ($user_index !== false) { $current_user = $leaderboard[$user_index]; $next_user = $user_index > 0 ? $leaderboard[$user_index - 1] : null; } else { // User is not in top 100 $user_row = $db->query(" SELECT user_id, user_name, user_points, user_registered, (SELECT COUNT(*) + 1 FROM users WHERE user_points > u.user_points) AS user_rank FROM users u WHERE user_id = '{$user_id}' LIMIT 1 ") or _error(SQL_ERROR_THROWEN); if ($user_row->num_rows > 0) { $current_user = $user_row->fetch_assoc(); $current_user['badges'] = $this->assignBadges($current_user); // Next user $next_row = $db->query(" SELECT user_name, user_points FROM users WHERE user_points > '{$current_user['user_points']}' ORDER BY user_points ASC LIMIT 1 ") or _error(SQL_ERROR_THROWEN); if ($next_row->num_rows > 0) { $next_user = $next_row->fetch_assoc(); } } } return [ 'top_users' => $leaderboard, 'current_user' => $current_user, 'next_user' => $next_user ];}public static function getAllBadges(){ $site_launch = '2025-05-24'; return [ [ 'key' => 'veteran', 'label' => 'Veteran', 'icon' => '/content/uploads/leaderboard/veteran.svg', 'description' => 'Registered for over 1 year.', 'requirement' => 'Account age ≥ 1 year', 'unlocked' => function($user) { $years = (strtotime('now') - strtotime($user['user_registered'])) / (60 * 60 * 24 * 365); return $years >= 1; }, 'level' => function($user) { $years = (strtotime('now') - strtotime($user['user_registered'])) / (60 * 60 * 24 * 365); if ($years >= 3) return 'gold'; if ($years >= 2) return 'silver'; if ($years >= 1) return 'bronze'; return null; } ], [ 'key' => 'top_points', 'label' => 'Active Member', 'icon' => '/content/uploads/leaderboard/contributor.svg', 'description' => 'Earned at least 1000 points.', 'requirement' => 'Points ≥ 1000', 'unlocked' => function($user) { return $user['user_points'] >= 1000; }, 'level' => function($user) { if ($user['user_points'] >= 5000) return 'gold'; if ($user['user_points'] >= 2500) return 'silver'; if ($user['user_points'] >= 1000) return 'bronze'; return null; } ], [ 'key' => 'early_supporter', 'label' => 'Supporter', 'icon' => '/content/uploads/leaderboard/early.svg', 'description' => 'Joined in the first 3 months.', 'requirement' => 'Registered before 2025-08-24', 'unlocked' => function($user) use ($site_launch) { return strtotime($user['user_registered']) <= strtotime("$site_launch +3 months"); }, 'level' => function($user) use ($site_launch) { $joined = strtotime($user['user_registered']); if ($joined <= strtotime("$site_launch +1 month")) return 'gold'; if ($joined <= strtotime("$site_launch +2 months")) return 'silver'; if ($joined <= strtotime("$site_launch +3 months")) return 'bronze'; return null; } ] ];}public static function assignBadges($user){ $all = self::getAllBadges(); $user_badges = []; foreach ($all as $badge) { $badge_levels = []; if ($badge['key'] === 'top_points') { $badge_levels = [ 'bronze' => [ 'requirement' => 1000, 'unlocked' => $user['user_points'] >= 1000, 'progress' => isset($user['user_points']) ? (int)$user['user_points'] : 0, ], 'silver' => [ 'requirement' => 2500, 'unlocked' => $user['user_points'] >= 2500, 'progress' => isset($user['user_points']) ? (int)$user['user_points'] : 0, ], 'gold' => [ 'requirement' => 5000, 'unlocked' => $user['user_points'] >= 5000, 'progress' => isset($user['user_points']) ? (int)$user['user_points'] : 0, ], ]; } if ($badge['key'] === 'veteran') { $years = (time() - strtotime($user['user_registered'])) / (60 * 60 * 24 * 365); $badge_levels = [ 'bronze' => [ 'requirement' => 1, 'unlocked' => $years >= 1, 'progress' => $years, ], 'silver' => [ 'requirement' => 2, 'unlocked' => $years >= 2, 'progress' => $years, ], 'gold' => [ 'requirement' => 3, 'unlocked' => $years >= 3, 'progress' => $years, ], ]; } if ($badge['key'] === 'early_supporter') { $registered = strtotime($user['user_registered']); $badge_levels = [ 'bronze' => [ 'requirement' => strtotime("2025-08-24"), 'progress' => $registered, 'unlocked' => $registered <= strtotime("2025-08-24"), ], 'silver' => [ 'requirement' => strtotime("2025-07-24"), 'progress' => $registered, 'unlocked' => $registered <= strtotime("2025-07-24"), ], 'gold' => [ 'requirement' => strtotime("2025-06-24"), 'progress' => $registered, 'unlocked' => $registered <= strtotime("2025-06-24"), ], ]; } $unlocked_level = null; foreach (['gold', 'silver', 'bronze'] as $lvl) { if (!empty($badge_levels[$lvl]['unlocked'])) { $unlocked_level = $lvl; break; } } $user_badges[] = [ 'key' => $badge['key'], 'label' => $badge['label'], 'icon' => $badge['icon'], 'description' => $badge['description'], 'requirement' => $badge['requirement'], 'unlocked' => call_user_func($badge['unlocked'], $user), 'unlocked_level' => $unlocked_level, 'levels' => $badge_levels ]; } return $user_badges; }leaderboard.php
<?php/** * notifications * * @package Sngine * @author Zamblek */// fetch bootloaderrequire('bootloader.php');// user accessuser_access();// page headerpage_header(__("TOP 100 ACTIVE MEMBERS"));try { $leaderboard_data = $user->getLeaderboardData($user->_data['user_id']); $smarty->assign('leaderboard', $leaderboard_data); $user_badges = User::assignBadges($user->_data); $smarty->assign('user_badges', $user_badges);} catch (Exception $e) { _error(__("Error"), $e->getMessage());}// page footerpage_footer('leaderboard');<h4 class="mt-3 mb-3 text-center">{__("TOP 100 ACTIVE MEMBERS")} (<a href="{$system['system_url']}/settings/points">INFO</a>)</h4>
<div class="badge-gallery"> {foreach from=$user_badges item=badge} <div class="badgepp badge-container"> {if $badge.unlocked_level} <img src="{$system['system_url']}/content/uploads/leaderboard/{$badge.key}_{$badge.unlocked_level}.png" class="badge-iconpp" width="40" height="40" /> {else} <h6 class="text-uppercase text-muted">{__("No badge unlocked")}</h6> {/if}
<div class="badge-info"> <h5 class="mb-1">{__($badge.label)}</h5> <p class="text-muted mb-2">{__($badge.description)}</p>
{foreach from=$badge.levels key=level item=level_data} <div class="badge-level-entry badge-level-{$level} {if $level_data.unlocked}unlocked{else}locked{/if}"> <img src="{$system['system_url']}/content/uploads/leaderboard/{$badge.key}_{$level}.png" alt="{$level}" class="me-1" width="20" height="20" /> <strong class="text-capitalize">{$level}</strong> –
{if $badge.key == 'veteran'} <strong>{$level_data.requirement} {__("years")}</strong> {if !$level_data.unlocked} – <strong class="text-danger">{__("Needs")} {math equation="x - y" x=$level_data.requirement y=$level_data.progress format="%.1f"} {__("years")}</strong> {/if} {elseif $badge.key == 'early_supporter'} {__("Joined before")} {$level_data.requirement|date_format:"%e %B, %Y"} {if !$level_data.unlocked} – <span class="text-danger">{__("Too late")}</span> {/if}
{else} (<strong>{$level_data.requirement} {__("points")}</strong>) {if !$level_data.unlocked} – <strong class="text-danger">{__("Needs")} {math equation="x - y" x=$level_data.requirement y=$level_data.progress|default:0 format="%d"}</strong> {/if} {/if} </div> {/foreach} </div> </div> {/foreach} </div>
{if $leaderboard.current_user} <div class="alert alert-info mt-4"> {__("You are ranked")} <strong>#{$leaderboard.current_user.user_rank}</strong> {__("with")} <strong>{$leaderboard.current_user.user_points}</strong> {__("points")}. {if $leaderboard.next_user} <br> {__("Next rank")}: <strong>{$leaderboard.next_user.user_name}</strong> {__("with")} <strong>{$leaderboard.next_user.user_points}</strong> {__("points")}. <br> {__("Needed to pass")}: <strong>{$leaderboard.next_user.user_points - $leaderboard.current_user.user_points + 1}</strong> {__("points")} {/if} </div> {/if}
<div class="leaderboardp">
{foreach $leaderboard.top_users as $user} <a href="{$system['system_url']}/{$user.user_name}"> <div class="user-rowp {if $user.user_id == $leaderboard.current_user.user_id}highlightp{/if}"> <div class="rankp"> {if $user.user_rank == 1}<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" viewBox="0 0 32 32"><g fill="none"><path fill="#0074ba" d="m18.768 11.51l-5.22-8.58c-.34-.58-.95-.93-1.62-.93h-6.59c-1.45 0-2.36 1.56-1.65 2.82a16.7 16.7 0 0 0 5.43 5.78c.76.59 1.7.91 2.67.91z"/><path fill="#00a6ed" d="M26.658 2h-6.59c-.67 0-1.28.35-1.62.93l-5.22 8.58h6.99c.97 0 1.9-.32 2.67-.91c2.25-1.46 4.11-3.44 5.43-5.78c.7-1.26-.21-2.82-1.66-2.82"/><path fill="#ffb02e" d="M15.99 30c5.545 0 10.04-4.607 10.04-10.29S21.535 9.42 15.99 9.42S5.95 14.027 5.95 19.71S10.445 30 15.99 30"/><path fill="#6d4534" d="M14.076 16.041a1 1 0 0 1 1-1H16a1 1 0 0 1 1 1V23a1 1 0 1 1-2 0v-5.962a1 1 0 0 1-.924-.997"/><path fill="#fcd53f" d="M16 28.76c-2.36 0-4.58-.94-6.24-2.65a9.1 9.1 0 0 1-2.59-6.4c0-2.42.92-4.69 2.59-6.4a8.69 8.69 0 0 1 12.49 0c3.44 3.53 3.44 9.27 0 12.8c-1.68 1.71-3.9 2.65-6.25 2.65m-.01-16.87c-1.95 0-3.91.76-5.39 2.29a7.87 7.87 0 0 0-2.23 5.53c0 2.09.79 4.05 2.23 5.53a7.48 7.48 0 0 0 5.39 2.29c2.04 0 3.95-.81 5.39-2.29c2.97-3.05 2.97-8.01 0-11.06a7.46 7.46 0 0 0-5.39-2.29"/></g></svg> {elseif $user.user_rank == 2}<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" viewBox="0 0 32 32"><g fill="none"><path fill="#0074ba" d="m18.768 11.51l-5.22-8.58c-.34-.58-.95-.93-1.62-.93h-6.59c-1.45 0-2.36 1.56-1.65 2.82a16.7 16.7 0 0 0 5.43 5.78c.76.59 1.7.91 2.67.91z"/><path fill="#00a6ed" d="M26.658 2h-6.59c-.67 0-1.28.35-1.62.93l-5.22 8.58h6.99c.97 0 1.9-.32 2.67-.91c2.25-1.46 4.11-3.44 5.43-5.78c.7-1.26-.21-2.82-1.66-2.82"/><path fill="#bebebe" d="M15.96 30.001c5.545 0 10.04-4.607 10.04-10.29s-4.495-10.29-10.04-10.29s-10.04 4.607-10.04 10.29s4.495 10.29 10.04 10.29"/><path fill="#e6e6e6" d="M15.96 28.761c-2.36 0-4.58-.94-6.24-2.65a9.1 9.1 0 0 1-2.59-6.4c0-2.42.92-4.69 2.59-6.4a8.69 8.69 0 0 1 12.49 0c3.44 3.53 3.44 9.27 0 12.8c-1.68 1.71-3.9 2.65-6.25 2.65m-.01-16.87c-1.95 0-3.91.76-5.39 2.29a7.87 7.87 0 0 0-2.23 5.53c0 2.09.79 4.05 2.23 5.53a7.48 7.48 0 0 0 5.39 2.29c2.04 0 3.95-.81 5.39-2.29c2.97-3.05 2.97-8.01 0-11.06a7.46 7.46 0 0 0-5.39-2.29"/><path fill="#636363" d="M17.838 23.95h-3.97a1 1 0 0 1-.91-.58a1 1 0 0 1 .13-1.07l3.3-4.05c.26-.32.14-.66.1-.76a.64.64 0 0 0-.58-.4h-.05c-.32 0-.61.15-.8.41c-.32.45-.95.56-1.41.24a1.01 1.01 0 0 1-.24-1.41c.56-.79 1.47-1.26 2.44-1.26h.13c1.02.05 1.91.66 2.33 1.59c.43.96.29 2.05-.37 2.86l-1.95 2.4h1.84c.56 0 1.01.45 1.01 1.01s-.44 1.02-1 1.02"/></g></svg> {elseif $user.user_rank == 3}<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" viewBox="0 0 32 32"><g fill="none"><path fill="#0074ba" d="m18.768 11.51l-5.22-8.58c-.34-.58-.95-.93-1.62-.93h-6.59c-1.45 0-2.36 1.56-1.65 2.82a16.7 16.7 0 0 0 5.43 5.78c.76.59 1.7.91 2.67.91z"/><path fill="#00a6ed" d="M26.658 2h-6.59c-.67 0-1.28.35-1.62.93l-5.22 8.58h6.99c.97 0 1.9-.32 2.67-.91c2.25-1.46 4.11-3.44 5.43-5.78c.7-1.26-.21-2.82-1.66-2.82"/><path fill="#d3883e" d="M15.99 30c5.545 0 10.04-4.607 10.04-10.29S21.535 9.42 15.99 9.42S5.95 14.027 5.95 19.71S10.445 30 15.99 30"/><path fill="#f3ad61" d="M16 28.76c-2.36 0-4.58-.94-6.24-2.65a9.1 9.1 0 0 1-2.59-6.4c0-2.42.92-4.69 2.59-6.4a8.69 8.69 0 0 1 12.49 0c3.44 3.53 3.44 9.27 0 12.8c-1.68 1.71-3.9 2.65-6.25 2.65m-.01-16.87c-1.95 0-3.91.76-5.39 2.29a7.87 7.87 0 0 0-2.23 5.53c0 2.09.79 4.05 2.23 5.53a7.48 7.48 0 0 0 5.39 2.29c2.04 0 3.95-.81 5.39-2.29c2.97-3.05 2.97-8.01 0-11.06a7.46 7.46 0 0 0-5.39-2.29"/><path fill="#402a32" d="m17.402 18.275l1.048-1.81a.98.98 0 0 0 0-.977a.97.97 0 0 0-.844-.488h-3.092a.977.977 0 0 0 0 1.953h1.403l-.752 1.311a.96.96 0 0 0-.214.6c0 .54.438.977.977.977c.61 0 1.108.498 1.108 1.108s-.498 1.109-1.108 1.109c-.468 0-.885-.295-1.038-.733a.975.975 0 0 0-1.24-.59a.966.966 0 0 0-.59 1.241A3.06 3.06 0 0 0 15.928 24a3.053 3.053 0 0 0 3.05-3.05a3.07 3.07 0 0 0-1.576-2.675"/></g></svg> {elseif $user.user_rank >= 4} {$user.user_rank} {/if} </div> <div class="user-infop"> <div class="usernamep"> {if $user.user_id == $leaderboard.current_user.user_id} <span class="badge bg-primary ms-1">{__("YOU ARE HERE")}</span> {else} {$user.user_name} {/if} </div> <div class="badgesp"> {foreach $user.badges as $badge} {if $badge.unlocked_level} <img src="{$system['system_url']}/content/uploads/leaderboard/{$badge.key}_{$badge.unlocked_level}.png" class="badge-iconpp" width="20" height="20" /> {/if} {/foreach} </div> </div> <div class="pointsp">{$user.user_points}</div> </div> </a> {/foreach}
</div>
style.css
/* LEADERBOARD PUNCTE */.leaderboardp { max-width: 800px; margin: 0 auto; border-radius: 10px; overflow: hidden;}.rankp,.pointsp { display: flex; flex-direction: column; justify-content: center;}.user-rowp { box-sizing: border-box; display: flex; justify-content: space-between; align-items: center; /* <- This is the key change */ background: #fff; padding: 15px 20px; border-bottom: 1px solid #ddd; transition: background 0.3s;}body.night-mode .user-rowp { background: #212121; border-bottom: 1px solid #485259;}.user-rowp.highlightp { position: relative; background: #ffe9f0;}body.night-mode .user-rowp.highlightp { background: #1D1D1D;}.user-rowp.highlightp::before { content: ""; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background-color: #ff3399;}.rankp { font-weight: bold; min-width: 40px; color: #1D1D1D; font-size: 1.3rem;}body.night-mode .rankp { color: #f8f8f8;}.user-infop { flex-grow: 1; padding: 0 20px;}.usernamep { font-size: 1.1em; font-weight: bold; margin-bottom: 5px;}.badgesp { display: flex; flex-wrap: wrap; gap: 5px;}.badgep { font-size: 0.8em; padding: 3px 8px; border-radius: 12px; background: #ccc; color: #fff;}.badgep.unlockedp { background-color: #28a745;}.badgep.lockedp { background-color: #6c757d;}.pointsp { min-width: 100px; text-align: right; font-weight: bold; color: #28a745; font-size: 1.4rem;}.badge.lockedpp { opacity: 0.5;}.badgepp { padding: 5px; border-radius: 8px; display: flex; flex-direction: column; align-items: start; margin-bottom: 10px;}.badge-iconpp { display: inline-block; vertical-align: middle; width: 32px; height: 32px;}.locked { background-color: #e9ecef;}.badge-container { border: 1px solid #ddd; border-radius: 8px; padding: 15px; margin-bottom: 20px; background: #fff; display: flex; gap: 15px;}body.night-mode .badge-container { background: #212121; border: 1px solid #485259;}.badge-iconpp { flex-shrink: 0;}.badge-info { flex-grow: 1;}.badge-level-entry { margin-top: 5px;}.badge-level-entry.unlocked { color: #28a745; font-weight: bold;}.badge-level-entry.locked { color: #6c757d;}
I leave here for free my leaderboard + Badge
class-user.php
// LEADERBOARD public function getLeaderboardData($user_id) { global $db; // Fetch top 100 users with activity stats $result = $db->query(" SELECT user_id, user_name, user_points, user_registered, RANK() OVER (ORDER BY user_points DESC) AS user_rank FROM users WHERE user_points > 0 ORDER BY user_points DESC LIMIT 100 ") or _error(SQL_ERROR_THROWEN); $leaderboard = []; while ($row = $result->fetch_assoc()) { $row['badges'] = $this->assignBadges($row); $leaderboard[] = $row; } // Identify current user and rank $user_index = array_search($user_id, array_column($leaderboard, 'user_id')); $current_user = null; $next_user = null; if ($user_index !== false) { $current_user = $leaderboard[$user_index]; $next_user = $user_index > 0 ? $leaderboard[$user_index - 1] : null; } else { // User is not in top 100 $user_row = $db->query(" SELECT user_id, user_name, user_points, user_registered, (SELECT COUNT(*) + 1 FROM users WHERE user_points > u.user_points) AS user_rank FROM users u WHERE user_id = '{$user_id}' LIMIT 1 ") or _error(SQL_ERROR_THROWEN); if ($user_row->num_rows > 0) { $current_user = $user_row->fetch_assoc(); $current_user['badges'] = $this->assignBadges($current_user); // Next user $next_row = $db->query(" SELECT user_name, user_points FROM users WHERE user_points > '{$current_user['user_points']}' ORDER BY user_points ASC LIMIT 1 ") or _error(SQL_ERROR_THROWEN); if ($next_row->num_rows > 0) { $next_user = $next_row->fetch_assoc(); } } } return [ 'top_users' => $leaderboard, 'current_user' => $current_user, 'next_user' => $next_user ];}public static function getAllBadges(){ $site_launch = '2025-05-24'; return [ [ 'key' => 'veteran', 'label' => 'Veteran', 'icon' => '/content/uploads/leaderboard/veteran.svg', 'description' => 'Registered for over 1 year.', 'requirement' => 'Account age ≥ 1 year', 'unlocked' => function($user) { $years = (strtotime('now') - strtotime($user['user_registered'])) / (60 * 60 * 24 * 365); return $years >= 1; }, 'level' => function($user) { $years = (strtotime('now') - strtotime($user['user_registered'])) / (60 * 60 * 24 * 365); if ($years >= 3) return 'gold'; if ($years >= 2) return 'silver'; if ($years >= 1) return 'bronze'; return null; } ], [ 'key' => 'top_points', 'label' => 'Active Member', 'icon' => '/content/uploads/leaderboard/contributor.svg', 'description' => 'Earned at least 1000 points.', 'requirement' => 'Points ≥ 1000', 'unlocked' => function($user) { return $user['user_points'] >= 1000; }, 'level' => function($user) { if ($user['user_points'] >= 5000) return 'gold'; if ($user['user_points'] >= 2500) return 'silver'; if ($user['user_points'] >= 1000) return 'bronze'; return null; } ], [ 'key' => 'early_supporter', 'label' => 'Supporter', 'icon' => '/content/uploads/leaderboard/early.svg', 'description' => 'Joined in the first 3 months.', 'requirement' => 'Registered before 2025-08-24', 'unlocked' => function($user) use ($site_launch) { return strtotime($user['user_registered']) <= strtotime("$site_launch +3 months"); }, 'level' => function($user) use ($site_launch) { $joined = strtotime($user['user_registered']); if ($joined <= strtotime("$site_launch +1 month")) return 'gold'; if ($joined <= strtotime("$site_launch +2 months")) return 'silver'; if ($joined <= strtotime("$site_launch +3 months")) return 'bronze'; return null; } ] ];}public static function assignBadges($user){ $all = self::getAllBadges(); $user_badges = []; foreach ($all as $badge) { $badge_levels = []; if ($badge['key'] === 'top_points') { $badge_levels = [ 'bronze' => [ 'requirement' => 1000, 'unlocked' => $user['user_points'] >= 1000, 'progress' => isset($user['user_points']) ? (int)$user['user_points'] : 0, ], 'silver' => [ 'requirement' => 2500, 'unlocked' => $user['user_points'] >= 2500, 'progress' => isset($user['user_points']) ? (int)$user['user_points'] : 0, ], 'gold' => [ 'requirement' => 5000, 'unlocked' => $user['user_points'] >= 5000, 'progress' => isset($user['user_points']) ? (int)$user['user_points'] : 0, ], ]; } if ($badge['key'] === 'veteran') { $years = (time() - strtotime($user['user_registered'])) / (60 * 60 * 24 * 365); $badge_levels = [ 'bronze' => [ 'requirement' => 1, 'unlocked' => $years >= 1, 'progress' => $years, ], 'silver' => [ 'requirement' => 2, 'unlocked' => $years >= 2, 'progress' => $years, ], 'gold' => [ 'requirement' => 3, 'unlocked' => $years >= 3, 'progress' => $years, ], ]; } if ($badge['key'] === 'early_supporter') { $registered = strtotime($user['user_registered']); $badge_levels = [ 'bronze' => [ 'requirement' => strtotime("2025-08-24"), 'progress' => $registered, 'unlocked' => $registered <= strtotime("2025-08-24"), ], 'silver' => [ 'requirement' => strtotime("2025-07-24"), 'progress' => $registered, 'unlocked' => $registered <= strtotime("2025-07-24"), ], 'gold' => [ 'requirement' => strtotime("2025-06-24"), 'progress' => $registered, 'unlocked' => $registered <= strtotime("2025-06-24"), ], ]; } $unlocked_level = null; foreach (['gold', 'silver', 'bronze'] as $lvl) { if (!empty($badge_levels[$lvl]['unlocked'])) { $unlocked_level = $lvl; break; } } $user_badges[] = [ 'key' => $badge['key'], 'label' => $badge['label'], 'icon' => $badge['icon'], 'description' => $badge['description'], 'requirement' => $badge['requirement'], 'unlocked' => call_user_func($badge['unlocked'], $user), 'unlocked_level' => $unlocked_level, 'levels' => $badge_levels ]; } return $user_badges; }leaderboard.php
<?php/** * notifications * * @package Sngine * @author Zamblek */// fetch bootloaderrequire('bootloader.php');// user accessuser_access();// page headerpage_header(__("TOP 100 ACTIVE MEMBERS"));try { $leaderboard_data = $user->getLeaderboardData($user->_data['user_id']); $smarty->assign('leaderboard', $leaderboard_data); $user_badges = User::assignBadges($user->_data); $smarty->assign('user_badges', $user_badges);} catch (Exception $e) { _error(__("Error"), $e->getMessage());}// page footerpage_footer('leaderboard');<h4 class="mt-3 mb-3 text-center">{__("TOP 100 ACTIVE MEMBERS")} (<a href="{$system['system_url']}/settings/points">INFO</a>)</h4>
<div class="badge-gallery"> {foreach from=$user_badges item=badge} <div class="badgepp badge-container"> {if $badge.unlocked_level} <img src="{$system['system_url']}/content/uploads/leaderboard/{$badge.key}_{$badge.unlocked_level}.png" class="badge-iconpp" width="40" height="40" /> {else} <h6 class="text-uppercase text-muted">{__("No badge unlocked")}</h6> {/if}
<div class="badge-info"> <h5 class="mb-1">{__($badge.label)}</h5> <p class="text-muted mb-2">{__($badge.description)}</p>
{foreach from=$badge.levels key=level item=level_data} <div class="badge-level-entry badge-level-{$level} {if $level_data.unlocked}unlocked{else}locked{/if}"> <img src="{$system['system_url']}/content/uploads/leaderboard/{$badge.key}_{$level}.png" alt="{$level}" class="me-1" width="20" height="20" /> <strong class="text-capitalize">{$level}</strong> –
{if $badge.key == 'veteran'} <strong>{$level_data.requirement} {__("years")}</strong> {if !$level_data.unlocked} – <strong class="text-danger">{__("Needs")} {math equation="x - y" x=$level_data.requirement y=$level_data.progress format="%.1f"} {__("years")}</strong> {/if} {elseif $badge.key == 'early_supporter'} {__("Joined before")} {$level_data.requirement|date_format:"%e %B, %Y"} {if !$level_data.unlocked} – <span class="text-danger">{__("Too late")}</span> {/if}
{else} (<strong>{$level_data.requirement} {__("points")}</strong>) {if !$level_data.unlocked} – <strong class="text-danger">{__("Needs")} {math equation="x - y" x=$level_data.requirement y=$level_data.progress|default:0 format="%d"}</strong> {/if} {/if} </div> {/foreach} </div> </div> {/foreach} </div>
{if $leaderboard.current_user} <div class="alert alert-info mt-4"> {__("You are ranked")} <strong>#{$leaderboard.current_user.user_rank}</strong> {__("with")} <strong>{$leaderboard.current_user.user_points}</strong> {__("points")}. {if $leaderboard.next_user} <br> {__("Next rank")}: <strong>{$leaderboard.next_user.user_name}</strong> {__("with")} <strong>{$leaderboard.next_user.user_points}</strong> {__("points")}. <br> {__("Needed to pass")}: <strong>{$leaderboard.next_user.user_points - $leaderboard.current_user.user_points + 1}</strong> {__("points")} {/if} </div> {/if}
<div class="leaderboardp">
{foreach $leaderboard.top_users as $user} <a href="{$system['system_url']}/{$user.user_name}"> <div class="user-rowp {if $user.user_id == $leaderboard.current_user.user_id}highlightp{/if}"> <div class="rankp"> {if $user.user_rank == 1}<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" viewBox="0 0 32 32"><g fill="none"><path fill="#0074ba" d="m18.768 11.51l-5.22-8.58c-.34-.58-.95-.93-1.62-.93h-6.59c-1.45 0-2.36 1.56-1.65 2.82a16.7 16.7 0 0 0 5.43 5.78c.76.59 1.7.91 2.67.91z"/><path fill="#00a6ed" d="M26.658 2h-6.59c-.67 0-1.28.35-1.62.93l-5.22 8.58h6.99c.97 0 1.9-.32 2.67-.91c2.25-1.46 4.11-3.44 5.43-5.78c.7-1.26-.21-2.82-1.66-2.82"/><path fill="#ffb02e" d="M15.99 30c5.545 0 10.04-4.607 10.04-10.29S21.535 9.42 15.99 9.42S5.95 14.027 5.95 19.71S10.445 30 15.99 30"/><path fill="#6d4534" d="M14.076 16.041a1 1 0 0 1 1-1H16a1 1 0 0 1 1 1V23a1 1 0 1 1-2 0v-5.962a1 1 0 0 1-.924-.997"/><path fill="#fcd53f" d="M16 28.76c-2.36 0-4.58-.94-6.24-2.65a9.1 9.1 0 0 1-2.59-6.4c0-2.42.92-4.69 2.59-6.4a8.69 8.69 0 0 1 12.49 0c3.44 3.53 3.44 9.27 0 12.8c-1.68 1.71-3.9 2.65-6.25 2.65m-.01-16.87c-1.95 0-3.91.76-5.39 2.29a7.87 7.87 0 0 0-2.23 5.53c0 2.09.79 4.05 2.23 5.53a7.48 7.48 0 0 0 5.39 2.29c2.04 0 3.95-.81 5.39-2.29c2.97-3.05 2.97-8.01 0-11.06a7.46 7.46 0 0 0-5.39-2.29"/></g></svg> {elseif $user.user_rank == 2}<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" viewBox="0 0 32 32"><g fill="none"><path fill="#0074ba" d="m18.768 11.51l-5.22-8.58c-.34-.58-.95-.93-1.62-.93h-6.59c-1.45 0-2.36 1.56-1.65 2.82a16.7 16.7 0 0 0 5.43 5.78c.76.59 1.7.91 2.67.91z"/><path fill="#00a6ed" d="M26.658 2h-6.59c-.67 0-1.28.35-1.62.93l-5.22 8.58h6.99c.97 0 1.9-.32 2.67-.91c2.25-1.46 4.11-3.44 5.43-5.78c.7-1.26-.21-2.82-1.66-2.82"/><path fill="#bebebe" d="M15.96 30.001c5.545 0 10.04-4.607 10.04-10.29s-4.495-10.29-10.04-10.29s-10.04 4.607-10.04 10.29s4.495 10.29 10.04 10.29"/><path fill="#e6e6e6" d="M15.96 28.761c-2.36 0-4.58-.94-6.24-2.65a9.1 9.1 0 0 1-2.59-6.4c0-2.42.92-4.69 2.59-6.4a8.69 8.69 0 0 1 12.49 0c3.44 3.53 3.44 9.27 0 12.8c-1.68 1.71-3.9 2.65-6.25 2.65m-.01-16.87c-1.95 0-3.91.76-5.39 2.29a7.87 7.87 0 0 0-2.23 5.53c0 2.09.79 4.05 2.23 5.53a7.48 7.48 0 0 0 5.39 2.29c2.04 0 3.95-.81 5.39-2.29c2.97-3.05 2.97-8.01 0-11.06a7.46 7.46 0 0 0-5.39-2.29"/><path fill="#636363" d="M17.838 23.95h-3.97a1 1 0 0 1-.91-.58a1 1 0 0 1 .13-1.07l3.3-4.05c.26-.32.14-.66.1-.76a.64.64 0 0 0-.58-.4h-.05c-.32 0-.61.15-.8.41c-.32.45-.95.56-1.41.24a1.01 1.01 0 0 1-.24-1.41c.56-.79 1.47-1.26 2.44-1.26h.13c1.02.05 1.91.66 2.33 1.59c.43.96.29 2.05-.37 2.86l-1.95 2.4h1.84c.56 0 1.01.45 1.01 1.01s-.44 1.02-1 1.02"/></g></svg> {elseif $user.user_rank == 3}<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" viewBox="0 0 32 32"><g fill="none"><path fill="#0074ba" d="m18.768 11.51l-5.22-8.58c-.34-.58-.95-.93-1.62-.93h-6.59c-1.45 0-2.36 1.56-1.65 2.82a16.7 16.7 0 0 0 5.43 5.78c.76.59 1.7.91 2.67.91z"/><path fill="#00a6ed" d="M26.658 2h-6.59c-.67 0-1.28.35-1.62.93l-5.22 8.58h6.99c.97 0 1.9-.32 2.67-.91c2.25-1.46 4.11-3.44 5.43-5.78c.7-1.26-.21-2.82-1.66-2.82"/><path fill="#d3883e" d="M15.99 30c5.545 0 10.04-4.607 10.04-10.29S21.535 9.42 15.99 9.42S5.95 14.027 5.95 19.71S10.445 30 15.99 30"/><path fill="#f3ad61" d="M16 28.76c-2.36 0-4.58-.94-6.24-2.65a9.1 9.1 0 0 1-2.59-6.4c0-2.42.92-4.69 2.59-6.4a8.69 8.69 0 0 1 12.49 0c3.44 3.53 3.44 9.27 0 12.8c-1.68 1.71-3.9 2.65-6.25 2.65m-.01-16.87c-1.95 0-3.91.76-5.39 2.29a7.87 7.87 0 0 0-2.23 5.53c0 2.09.79 4.05 2.23 5.53a7.48 7.48 0 0 0 5.39 2.29c2.04 0 3.95-.81 5.39-2.29c2.97-3.05 2.97-8.01 0-11.06a7.46 7.46 0 0 0-5.39-2.29"/><path fill="#402a32" d="m17.402 18.275l1.048-1.81a.98.98 0 0 0 0-.977a.97.97 0 0 0-.844-.488h-3.092a.977.977 0 0 0 0 1.953h1.403l-.752 1.311a.96.96 0 0 0-.214.6c0 .54.438.977.977.977c.61 0 1.108.498 1.108 1.108s-.498 1.109-1.108 1.109c-.468 0-.885-.295-1.038-.733a.975.975 0 0 0-1.24-.59a.966.966 0 0 0-.59 1.241A3.06 3.06 0 0 0 15.928 24a3.053 3.053 0 0 0 3.05-3.05a3.07 3.07 0 0 0-1.576-2.675"/></g></svg> {elseif $user.user_rank >= 4} {$user.user_rank} {/if} </div> <div class="user-infop"> <div class="usernamep"> {if $user.user_id == $leaderboard.current_user.user_id} <span class="badge bg-primary ms-1">{__("YOU ARE HERE")}</span> {else} {$user.user_name} {/if} </div> <div class="badgesp"> {foreach $user.badges as $badge} {if $badge.unlocked_level} <img src="{$system['system_url']}/content/uploads/leaderboard/{$badge.key}_{$badge.unlocked_level}.png" class="badge-iconpp" width="20" height="20" /> {/if} {/foreach} </div> </div> <div class="pointsp">{$user.user_points}</div> </div> </a> {/foreach}
</div>
style.css
/* LEADERBOARD PUNCTE */.leaderboardp { max-width: 800px; margin: 0 auto; border-radius: 10px; overflow: hidden;}.rankp,.pointsp { display: flex; flex-direction: column; justify-content: center;}.user-rowp { box-sizing: border-box; display: flex; justify-content: space-between; align-items: center; /* <- This is the key change */ background: #fff; padding: 15px 20px; border-bottom: 1px solid #ddd; transition: background 0.3s;}body.night-mode .user-rowp { background: #212121; border-bottom: 1px solid #485259;}.user-rowp.highlightp { position: relative; background: #ffe9f0;}body.night-mode .user-rowp.highlightp { background: #1D1D1D;}.user-rowp.highlightp::before { content: ""; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background-color: #ff3399;}.rankp { font-weight: bold; min-width: 40px; color: #1D1D1D; font-size: 1.3rem;}body.night-mode .rankp { color: #f8f8f8;}.user-infop { flex-grow: 1; padding: 0 20px;}.usernamep { font-size: 1.1em; font-weight: bold; margin-bottom: 5px;}.badgesp { display: flex; flex-wrap: wrap; gap: 5px;}.badgep { font-size: 0.8em; padding: 3px 8px; border-radius: 12px; background: #ccc; color: #fff;}.badgep.unlockedp { background-color: #28a745;}.badgep.lockedp { background-color: #6c757d;}.pointsp { min-width: 100px; text-align: right; font-weight: bold; color: #28a745; font-size: 1.4rem;}.badge.lockedpp { opacity: 0.5;}.badgepp { padding: 5px; border-radius: 8px; display: flex; flex-direction: column; align-items: start; margin-bottom: 10px;}.badge-iconpp { display: inline-block; vertical-align: middle; width: 32px; height: 32px;}.locked { background-color: #e9ecef;}.badge-container { border: 1px solid #ddd; border-radius: 8px; padding: 15px; margin-bottom: 20px; background: #fff; display: flex; gap: 15px;}body.night-mode .badge-container { background: #212121; border: 1px solid #485259;}.badge-iconpp { flex-shrink: 0;}.badge-info { flex-grow: 1;}.badge-level-entry { margin-top: 5px;}.badge-level-entry.unlocked { color: #28a745; font-weight: bold;}.badge-level-entry.locked { color: #6c757d;}
I leave here for free my leaderboard + Badge
class-user.php
// LEADERBOARD public function getLeaderboardData($user_id) { global $db; // Fetch top 100 users with activity stats $result = $db->query(" SELECT user_id, user_name, user_points, user_registered, RANK() OVER (ORDER BY user_points DESC) AS user_rank FROM users WHERE user_points > 0 ORDER BY user_points DESC LIMIT 100 ") or _error(SQL_ERROR_THROWEN); $leaderboard = []; while ($row = $result->fetch_assoc()) { $row['badges'] = $this->assignBadges($row); $leaderboard[] = $row; } // Identify current user and rank $user_index = array_search($user_id, array_column($leaderboard, 'user_id')); $current_user = null; $next_user = null; if ($user_index !== false) { $current_user = $leaderboard[$user_index]; $next_user = $user_index > 0 ? $leaderboard[$user_index - 1] : null; } else { // User is not in top 100 $user_row = $db->query(" SELECT user_id, user_name, user_points, user_registered, (SELECT COUNT(*) + 1 FROM users WHERE user_points > u.user_points) AS user_rank FROM users u WHERE user_id = '{$user_id}' LIMIT 1 ") or _error(SQL_ERROR_THROWEN); if ($user_row->num_rows > 0) { $current_user = $user_row->fetch_assoc(); $current_user['badges'] = $this->assignBadges($current_user); // Next user $next_row = $db->query(" SELECT user_name, user_points FROM users WHERE user_points > '{$current_user['user_points']}' ORDER BY user_points ASC LIMIT 1 ") or _error(SQL_ERROR_THROWEN); if ($next_row->num_rows > 0) { $next_user = $next_row->fetch_assoc(); } } } return [ 'top_users' => $leaderboard, 'current_user' => $current_user, 'next_user' => $next_user ];}public static function getAllBadges(){ $site_launch = '2025-05-24'; return [ [ 'key' => 'veteran', 'label' => 'Veteran', 'icon' => '/content/uploads/leaderboard/veteran.svg', 'description' => 'Registered for over 1 year.', 'requirement' => 'Account age ≥ 1 year', 'unlocked' => function($user) { $years = (strtotime('now') - strtotime($user['user_registered'])) / (60 * 60 * 24 * 365); return $years >= 1; }, 'level' => function($user) { $years = (strtotime('now') - strtotime($user['user_registered'])) / (60 * 60 * 24 * 365); if ($years >= 3) return 'gold'; if ($years >= 2) return 'silver'; if ($years >= 1) return 'bronze'; return null; } ], [ 'key' => 'top_points', 'label' => 'Active Member', 'icon' => '/content/uploads/leaderboard/contributor.svg', 'description' => 'Earned at least 1000 points.', 'requirement' => 'Points ≥ 1000', 'unlocked' => function($user) { return $user['user_points'] >= 1000; }, 'level' => function($user) { if ($user['user_points'] >= 5000) return 'gold'; if ($user['user_points'] >= 2500) return 'silver'; if ($user['user_points'] >= 1000) return 'bronze'; return null; } ], [ 'key' => 'early_supporter', 'label' => 'Supporter', 'icon' => '/content/uploads/leaderboard/early.svg', 'description' => 'Joined in the first 3 months.', 'requirement' => 'Registered before 2025-08-24', 'unlocked' => function($user) use ($site_launch) { return strtotime($user['user_registered']) <= strtotime("$site_launch +3 months"); }, 'level' => function($user) use ($site_launch) { $joined = strtotime($user['user_registered']); if ($joined <= strtotime("$site_launch +1 month")) return 'gold'; if ($joined <= strtotime("$site_launch +2 months")) return 'silver'; if ($joined <= strtotime("$site_launch +3 months")) return 'bronze'; return null; } ] ];}public static function assignBadges($user){ $all = self::getAllBadges(); $user_badges = []; foreach ($all as $badge) { $badge_levels = []; if ($badge['key'] === 'top_points') { $badge_levels = [ 'bronze' => [ 'requirement' => 1000, 'unlocked' => $user['user_points'] >= 1000, 'progress' => isset($user['user_points']) ? (int)$user['user_points'] : 0, ], 'silver' => [ 'requirement' => 2500, 'unlocked' => $user['user_points'] >= 2500, 'progress' => isset($user['user_points']) ? (int)$user['user_points'] : 0, ], 'gold' => [ 'requirement' => 5000, 'unlocked' => $user['user_points'] >= 5000, 'progress' => isset($user['user_points']) ? (int)$user['user_points'] : 0, ], ]; } if ($badge['key'] === 'veteran') { $years = (time() - strtotime($user['user_registered'])) / (60 * 60 * 24 * 365); $badge_levels = [ 'bronze' => [ 'requirement' => 1, 'unlocked' => $years >= 1, 'progress' => $years, ], 'silver' => [ 'requirement' => 2, 'unlocked' => $years >= 2, 'progress' => $years, ], 'gold' => [ 'requirement' => 3, 'unlocked' => $years >= 3, 'progress' => $years, ], ]; } if ($badge['key'] === 'early_supporter') { $registered = strtotime($user['user_registered']); $badge_levels = [ 'bronze' => [ 'requirement' => strtotime("2025-08-24"), 'progress' => $registered, 'unlocked' => $registered <= strtotime("2025-08-24"), ], 'silver' => [ 'requirement' => strtotime("2025-07-24"), 'progress' => $registered, 'unlocked' => $registered <= strtotime("2025-07-24"), ], 'gold' => [ 'requirement' => strtotime("2025-06-24"), 'progress' => $registered, 'unlocked' => $registered <= strtotime("2025-06-24"), ], ]; } $unlocked_level = null; foreach (['gold', 'silver', 'bronze'] as $lvl) { if (!empty($badge_levels[$lvl]['unlocked'])) { $unlocked_level = $lvl; break; } } $user_badges[] = [ 'key' => $badge['key'], 'label' => $badge['label'], 'icon' => $badge['icon'], 'description' => $badge['description'], 'requirement' => $badge['requirement'], 'unlocked' => call_user_func($badge['unlocked'], $user), 'unlocked_level' => $unlocked_level, 'levels' => $badge_levels ]; } return $user_badges; }leaderboard.php
<?php/** * notifications * * @package Sngine * @author Zamblek */// fetch bootloaderrequire('bootloader.php');// user accessuser_access();// page headerpage_header(__("TOP 100 ACTIVE MEMBERS"));try { $leaderboard_data = $user->getLeaderboardData($user->_data['user_id']); $smarty->assign('leaderboard', $leaderboard_data); $user_badges = User::assignBadges($user->_data); $smarty->assign('user_badges', $user_badges);} catch (Exception $e) { _error(__("Error"), $e->getMessage());}// page footerpage_footer('leaderboard');<h4 class="mt-3 mb-3 text-center">{__("TOP 100 ACTIVE MEMBERS")} (<a href="{$system['system_url']}/settings/points">INFO</a>)</h4>
<div class="badge-gallery"> {foreach from=$user_badges item=badge} <div class="badgepp badge-container"> {if $badge.unlocked_level} <img src="{$system['system_url']}/content/uploads/leaderboard/{$badge.key}_{$badge.unlocked_level}.png" class="badge-iconpp" width="40" height="40" /> {else} <h6 class="text-uppercase text-muted">{__("No badge unlocked")}</h6> {/if}
<div class="badge-info"> <h5 class="mb-1">{__($badge.label)}</h5> <p class="text-muted mb-2">{__($badge.description)}</p>
{foreach from=$badge.levels key=level item=level_data} <div class="badge-level-entry badge-level-{$level} {if $level_data.unlocked}unlocked{else}locked{/if}"> <img src="{$system['system_url']}/content/uploads/leaderboard/{$badge.key}_{$level}.png" alt="{$level}" class="me-1" width="20" height="20" /> <strong class="text-capitalize">{$level}</strong> –
{if $badge.key == 'veteran'} <strong>{$level_data.requirement} {__("years")}</strong> {if !$level_data.unlocked} – <strong class="text-danger">{__("Needs")} {math equation="x - y" x=$level_data.requirement y=$level_data.progress format="%.1f"} {__("years")}</strong> {/if} {elseif $badge.key == 'early_supporter'} {__("Joined before")} {$level_data.requirement|date_format:"%e %B, %Y"} {if !$level_data.unlocked} – <span class="text-danger">{__("Too late")}</span> {/if}
{else} (<strong>{$level_data.requirement} {__("points")}</strong>) {if !$level_data.unlocked} – <strong class="text-danger">{__("Needs")} {math equation="x - y" x=$level_data.requirement y=$level_data.progress|default:0 format="%d"}</strong> {/if} {/if} </div> {/foreach} </div> </div> {/foreach} </div>
{if $leaderboard.current_user} <div class="alert alert-info mt-4"> {__("You are ranked")} <strong>#{$leaderboard.current_user.user_rank}</strong> {__("with")} <strong>{$leaderboard.current_user.user_points}</strong> {__("points")}. {if $leaderboard.next_user} <br> {__("Next rank")}: <strong>{$leaderboard.next_user.user_name}</strong> {__("with")} <strong>{$leaderboard.next_user.user_points}</strong> {__("points")}. <br> {__("Needed to pass")}: <strong>{$leaderboard.next_user.user_points - $leaderboard.current_user.user_points + 1}</strong> {__("points")} {/if} </div> {/if}
<div class="leaderboardp">
{foreach $leaderboard.top_users as $user} <a href="{$system['system_url']}/{$user.user_name}"> <div class="user-rowp {if $user.user_id == $leaderboard.current_user.user_id}highlightp{/if}"> <div class="rankp"> {if $user.user_rank == 1}<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" viewBox="0 0 32 32"><g fill="none"><path fill="#0074ba" d="m18.768 11.51l-5.22-8.58c-.34-.58-.95-.93-1.62-.93h-6.59c-1.45 0-2.36 1.56-1.65 2.82a16.7 16.7 0 0 0 5.43 5.78c.76.59 1.7.91 2.67.91z"/><path fill="#00a6ed" d="M26.658 2h-6.59c-.67 0-1.28.35-1.62.93l-5.22 8.58h6.99c.97 0 1.9-.32 2.67-.91c2.25-1.46 4.11-3.44 5.43-5.78c.7-1.26-.21-2.82-1.66-2.82"/><path fill="#ffb02e" d="M15.99 30c5.545 0 10.04-4.607 10.04-10.29S21.535 9.42 15.99 9.42S5.95 14.027 5.95 19.71S10.445 30 15.99 30"/><path fill="#6d4534" d="M14.076 16.041a1 1 0 0 1 1-1H16a1 1 0 0 1 1 1V23a1 1 0 1 1-2 0v-5.962a1 1 0 0 1-.924-.997"/><path fill="#fcd53f" d="M16 28.76c-2.36 0-4.58-.94-6.24-2.65a9.1 9.1 0 0 1-2.59-6.4c0-2.42.92-4.69 2.59-6.4a8.69 8.69 0 0 1 12.49 0c3.44 3.53 3.44 9.27 0 12.8c-1.68 1.71-3.9 2.65-6.25 2.65m-.01-16.87c-1.95 0-3.91.76-5.39 2.29a7.87 7.87 0 0 0-2.23 5.53c0 2.09.79 4.05 2.23 5.53a7.48 7.48 0 0 0 5.39 2.29c2.04 0 3.95-.81 5.39-2.29c2.97-3.05 2.97-8.01 0-11.06a7.46 7.46 0 0 0-5.39-2.29"/></g></svg> {elseif $user.user_rank == 2}<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" viewBox="0 0 32 32"><g fill="none"><path fill="#0074ba" d="m18.768 11.51l-5.22-8.58c-.34-.58-.95-.93-1.62-.93h-6.59c-1.45 0-2.36 1.56-1.65 2.82a16.7 16.7 0 0 0 5.43 5.78c.76.59 1.7.91 2.67.91z"/><path fill="#00a6ed" d="M26.658 2h-6.59c-.67 0-1.28.35-1.62.93l-5.22 8.58h6.99c.97 0 1.9-.32 2.67-.91c2.25-1.46 4.11-3.44 5.43-5.78c.7-1.26-.21-2.82-1.66-2.82"/><path fill="#bebebe" d="M15.96 30.001c5.545 0 10.04-4.607 10.04-10.29s-4.495-10.29-10.04-10.29s-10.04 4.607-10.04 10.29s4.495 10.29 10.04 10.29"/><path fill="#e6e6e6" d="M15.96 28.761c-2.36 0-4.58-.94-6.24-2.65a9.1 9.1 0 0 1-2.59-6.4c0-2.42.92-4.69 2.59-6.4a8.69 8.69 0 0 1 12.49 0c3.44 3.53 3.44 9.27 0 12.8c-1.68 1.71-3.9 2.65-6.25 2.65m-.01-16.87c-1.95 0-3.91.76-5.39 2.29a7.87 7.87 0 0 0-2.23 5.53c0 2.09.79 4.05 2.23 5.53a7.48 7.48 0 0 0 5.39 2.29c2.04 0 3.95-.81 5.39-2.29c2.97-3.05 2.97-8.01 0-11.06a7.46 7.46 0 0 0-5.39-2.29"/><path fill="#636363" d="M17.838 23.95h-3.97a1 1 0 0 1-.91-.58a1 1 0 0 1 .13-1.07l3.3-4.05c.26-.32.14-.66.1-.76a.64.64 0 0 0-.58-.4h-.05c-.32 0-.61.15-.8.41c-.32.45-.95.56-1.41.24a1.01 1.01 0 0 1-.24-1.41c.56-.79 1.47-1.26 2.44-1.26h.13c1.02.05 1.91.66 2.33 1.59c.43.96.29 2.05-.37 2.86l-1.95 2.4h1.84c.56 0 1.01.45 1.01 1.01s-.44 1.02-1 1.02"/></g></svg> {elseif $user.user_rank == 3}<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" viewBox="0 0 32 32"><g fill="none"><path fill="#0074ba" d="m18.768 11.51l-5.22-8.58c-.34-.58-.95-.93-1.62-.93h-6.59c-1.45 0-2.36 1.56-1.65 2.82a16.7 16.7 0 0 0 5.43 5.78c.76.59 1.7.91 2.67.91z"/><path fill="#00a6ed" d="M26.658 2h-6.59c-.67 0-1.28.35-1.62.93l-5.22 8.58h6.99c.97 0 1.9-.32 2.67-.91c2.25-1.46 4.11-3.44 5.43-5.78c.7-1.26-.21-2.82-1.66-2.82"/><path fill="#d3883e" d="M15.99 30c5.545 0 10.04-4.607 10.04-10.29S21.535 9.42 15.99 9.42S5.95 14.027 5.95 19.71S10.445 30 15.99 30"/><path fill="#f3ad61" d="M16 28.76c-2.36 0-4.58-.94-6.24-2.65a9.1 9.1 0 0 1-2.59-6.4c0-2.42.92-4.69 2.59-6.4a8.69 8.69 0 0 1 12.49 0c3.44 3.53 3.44 9.27 0 12.8c-1.68 1.71-3.9 2.65-6.25 2.65m-.01-16.87c-1.95 0-3.91.76-5.39 2.29a7.87 7.87 0 0 0-2.23 5.53c0 2.09.79 4.05 2.23 5.53a7.48 7.48 0 0 0 5.39 2.29c2.04 0 3.95-.81 5.39-2.29c2.97-3.05 2.97-8.01 0-11.06a7.46 7.46 0 0 0-5.39-2.29"/><path fill="#402a32" d="m17.402 18.275l1.048-1.81a.98.98 0 0 0 0-.977a.97.97 0 0 0-.844-.488h-3.092a.977.977 0 0 0 0 1.953h1.403l-.752 1.311a.96.96 0 0 0-.214.6c0 .54.438.977.977.977c.61 0 1.108.498 1.108 1.108s-.498 1.109-1.108 1.109c-.468 0-.885-.295-1.038-.733a.975.975 0 0 0-1.24-.59a.966.966 0 0 0-.59 1.241A3.06 3.06 0 0 0 15.928 24a3.053 3.053 0 0 0 3.05-3.05a3.07 3.07 0 0 0-1.576-2.675"/></g></svg> {elseif $user.user_rank >= 4} {$user.user_rank} {/if} </div> <div class="user-infop"> <div class="usernamep"> {if $user.user_id == $leaderboard.current_user.user_id} <span class="badge bg-primary ms-1">{__("YOU ARE HERE")}</span> {else} {$user.user_name} {/if} </div> <div class="badgesp"> {foreach $user.badges as $badge} {if $badge.unlocked_level} <img src="{$system['system_url']}/content/uploads/leaderboard/{$badge.key}_{$badge.unlocked_level}.png" class="badge-iconpp" width="20" height="20" /> {/if} {/foreach} </div> </div> <div class="pointsp">{$user.user_points}</div> </div> </a> {/foreach}
</div>
style.css
/* LEADERBOARD PUNCTE */.leaderboardp { max-width: 800px; margin: 0 auto; border-radius: 10px; overflow: hidden;}.rankp,.pointsp { display: flex; flex-direction: column; justify-content: center;}.user-rowp { box-sizing: border-box; display: flex; justify-content: space-between; align-items: center; /* <- This is the key change */ background: #fff; padding: 15px 20px; border-bottom: 1px solid #ddd; transition: background 0.3s;}body.night-mode .user-rowp { background: #212121; border-bottom: 1px solid #485259;}.user-rowp.highlightp { position: relative; background: #ffe9f0;}body.night-mode .user-rowp.highlightp { background: #1D1D1D;}.user-rowp.highlightp::before { content: ""; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background-color: #ff3399;}.rankp { font-weight: bold; min-width: 40px; color: #1D1D1D; font-size: 1.3rem;}body.night-mode .rankp { color: #f8f8f8;}.user-infop { flex-grow: 1; padding: 0 20px;}.usernamep { font-size: 1.1em; font-weight: bold; margin-bottom: 5px;}.badgesp { display: flex; flex-wrap: wrap; gap: 5px;}.badgep { font-size: 0.8em; padding: 3px 8px; border-radius: 12px; background: #ccc; color: #fff;}.badgep.unlockedp { background-color: #28a745;}.badgep.lockedp { background-color: #6c757d;}.pointsp { min-width: 100px; text-align: right; font-weight: bold; color: #28a745; font-size: 1.4rem;}.badge.lockedpp { opacity: 0.5;}.badgepp { padding: 5px; border-radius: 8px; display: flex; flex-direction: column; align-items: start; margin-bottom: 10px;}.badge-iconpp { display: inline-block; vertical-align: middle; width: 32px; height: 32px;}.locked { background-color: #e9ecef;}.badge-container { border: 1px solid #ddd; border-radius: 8px; padding: 15px; margin-bottom: 20px; background: #fff; display: flex; gap: 15px;}body.night-mode .badge-container { background: #212121; border: 1px solid #485259;}.badge-iconpp { flex-shrink: 0;}.badge-info { flex-grow: 1;}.badge-level-entry { margin-top: 5px;}.badge-level-entry.unlocked { color: #28a745; font-weight: bold;}.badge-level-entry.locked { color: #6c757d;}
I leave here for free my leaderboard + Badge
class-user.php
// LEADERBOARD public function getLeaderboardData($user_id) { global $db; // Fetch top 100 users with activity stats $result = $db->query(" SELECT user_id, user_name, user_points, user_registered, RANK() OVER (ORDER BY user_points DESC) AS user_rank FROM users WHERE user_points > 0 ORDER BY user_points DESC LIMIT 100 ") or _error(SQL_ERROR_THROWEN); $leaderboard = []; while ($row = $result->fetch_assoc()) { $row['badges'] = $this->assignBadges($row); $leaderboard[] = $row; } // Identify current user and rank $user_index = array_search($user_id, array_column($leaderboard, 'user_id')); $current_user = null; $next_user = null; if ($user_index !== false) { $current_user = $leaderboard[$user_index]; $next_user = $user_index > 0 ? $leaderboard[$user_index - 1] : null; } else { // User is not in top 100 $user_row = $db->query(" SELECT user_id, user_name, user_points, user_registered, (SELECT COUNT(*) + 1 FROM users WHERE user_points > u.user_points) AS user_rank FROM users u WHERE user_id = '{$user_id}' LIMIT 1 ") or _error(SQL_ERROR_THROWEN); if ($user_row->num_rows > 0) { $current_user = $user_row->fetch_assoc(); $current_user['badges'] = $this->assignBadges($current_user); // Next user $next_row = $db->query(" SELECT user_name, user_points FROM users WHERE user_points > '{$current_user['user_points']}' ORDER BY user_points ASC LIMIT 1 ") or _error(SQL_ERROR_THROWEN); if ($next_row->num_rows > 0) { $next_user = $next_row->fetch_assoc(); } } } return [ 'top_users' => $leaderboard, 'current_user' => $current_user, 'next_user' => $next_user ];}public static function getAllBadges(){ $site_launch = '2025-05-24'; return [ [ 'key' => 'veteran', 'label' => 'Veteran', 'icon' => '/content/uploads/leaderboard/veteran.svg', 'description' => 'Registered for over 1 year.', 'requirement' => 'Account age ≥ 1 year', 'unlocked' => function($user) { $years = (strtotime('now') - strtotime($user['user_registered'])) / (60 * 60 * 24 * 365); return $years >= 1; }, 'level' => function($user) { $years = (strtotime('now') - strtotime($user['user_registered'])) / (60 * 60 * 24 * 365); if ($years >= 3) return 'gold'; if ($years >= 2) return 'silver'; if ($years >= 1) return 'bronze'; return null; } ], [ 'key' => 'top_points', 'label' => 'Active Member', 'icon' => '/content/uploads/leaderboard/contributor.svg', 'description' => 'Earned at least 1000 points.', 'requirement' => 'Points ≥ 1000', 'unlocked' => function($user) { return $user['user_points'] >= 1000; }, 'level' => function($user) { if ($user['user_points'] >= 5000) return 'gold'; if ($user['user_points'] >= 2500) return 'silver'; if ($user['user_points'] >= 1000) return 'bronze'; return null; } ], [ 'key' => 'early_supporter', 'label' => 'Supporter', 'icon' => '/content/uploads/leaderboard/early.svg', 'description' => 'Joined in the first 3 months.', 'requirement' => 'Registered before 2025-08-24', 'unlocked' => function($user) use ($site_launch) { return strtotime($user['user_registered']) <= strtotime("$site_launch +3 months"); }, 'level' => function($user) use ($site_launch) { $joined = strtotime($user['user_registered']); if ($joined <= strtotime("$site_launch +1 month")) return 'gold'; if ($joined <= strtotime("$site_launch +2 months")) return 'silver'; if ($joined <= strtotime("$site_launch +3 months")) return 'bronze'; return null; } ] ];}public static function assignBadges($user){ $all = self::getAllBadges(); $user_badges = []; foreach ($all as $badge) { $badge_levels = []; if ($badge['key'] === 'top_points') { $badge_levels = [ 'bronze' => [ 'requirement' => 1000, 'unlocked' => $user['user_points'] >= 1000, 'progress' => isset($user['user_points']) ? (int)$user['user_points'] : 0, ], 'silver' => [ 'requirement' => 2500, 'unlocked' => $user['user_points'] >= 2500, 'progress' => isset($user['user_points']) ? (int)$user['user_points'] : 0, ], 'gold' => [ 'requirement' => 5000, 'unlocked' => $user['user_points'] >= 5000, 'progress' => isset($user['user_points']) ? (int)$user['user_points'] : 0, ], ]; } if ($badge['key'] === 'veteran') { $years = (time() - strtotime($user['user_registered'])) / (60 * 60 * 24 * 365); $badge_levels = [ 'bronze' => [ 'requirement' => 1, 'unlocked' => $years >= 1, 'progress' => $years, ], 'silver' => [ 'requirement' => 2, 'unlocked' => $years >= 2, 'progress' => $years, ], 'gold' => [ 'requirement' => 3, 'unlocked' => $years >= 3, 'progress' => $years, ], ]; } if ($badge['key'] === 'early_supporter') { $registered = strtotime($user['user_registered']); $badge_levels = [ 'bronze' => [ 'requirement' => strtotime("2025-08-24"), 'progress' => $registered, 'unlocked' => $registered <= strtotime("2025-08-24"), ], 'silver' => [ 'requirement' => strtotime("2025-07-24"), 'progress' => $registered, 'unlocked' => $registered <= strtotime("2025-07-24"), ], 'gold' => [ 'requirement' => strtotime("2025-06-24"), 'progress' => $registered, 'unlocked' => $registered <= strtotime("2025-06-24"), ], ]; } $unlocked_level = null; foreach (['gold', 'silver', 'bronze'] as $lvl) { if (!empty($badge_levels[$lvl]['unlocked'])) { $unlocked_level = $lvl; break; } } $user_badges[] = [ 'key' => $badge['key'], 'label' => $badge['label'], 'icon' => $badge['icon'], 'description' => $badge['description'], 'requirement' => $badge['requirement'], 'unlocked' => call_user_func($badge['unlocked'], $user), 'unlocked_level' => $unlocked_level, 'levels' => $badge_levels ]; } return $user_badges; }leaderboard.php
<?php/** * notifications * * @package Sngine * @author Zamblek */// fetch bootloaderrequire('bootloader.php');// user accessuser_access();// page headerpage_header(__("TOP 100 ACTIVE MEMBERS"));try { $leaderboard_data = $user->getLeaderboardData($user->_data['user_id']); $smarty->assign('leaderboard', $leaderboard_data); $user_badges = User::assignBadges($user->_data); $smarty->assign('user_badges', $user_badges);} catch (Exception $e) { _error(__("Error"), $e->getMessage());}// page footerpage_footer('leaderboard');<h4 class="mt-3 mb-3 text-center">{__("TOP 100 ACTIVE MEMBERS")} (<a href="{$system['system_url']}/settings/points">INFO</a>)</h4>
<div class="badge-gallery"> {foreach from=$user_badges item=badge} <div class="badgepp badge-container"> {if $badge.unlocked_level} <img src="{$system['system_url']}/content/uploads/leaderboard/{$badge.key}_{$badge.unlocked_level}.png" class="badge-iconpp" width="40" height="40" /> {else} <h6 class="text-uppercase text-muted">{__("No badge unlocked")}</h6> {/if}
<div class="badge-info"> <h5 class="mb-1">{__($badge.label)}</h5> <p class="text-muted mb-2">{__($badge.description)}</p>
{foreach from=$badge.levels key=level item=level_data} <div class="badge-level-entry badge-level-{$level} {if $level_data.unlocked}unlocked{else}locked{/if}"> <img src="{$system['system_url']}/content/uploads/leaderboard/{$badge.key}_{$level}.png" alt="{$level}" class="me-1" width="20" height="20" /> <strong class="text-capitalize">{$level}</strong> –
{if $badge.key == 'veteran'} <strong>{$level_data.requirement} {__("years")}</strong> {if !$level_data.unlocked} – <strong class="text-danger">{__("Needs")} {math equation="x - y" x=$level_data.requirement y=$level_data.progress format="%.1f"} {__("years")}</strong> {/if} {elseif $badge.key == 'early_supporter'} {__("Joined before")} {$level_data.requirement|date_format:"%e %B, %Y"} {if !$level_data.unlocked} – <span class="text-danger">{__("Too late")}</span> {/if}
{else} (<strong>{$level_data.requirement} {__("points")}</strong>) {if !$level_data.unlocked} – <strong class="text-danger">{__("Needs")} {math equation="x - y" x=$level_data.requirement y=$level_data.progress|default:0 format="%d"}</strong> {/if} {/if} </div> {/foreach} </div> </div> {/foreach} </div>
{if $leaderboard.current_user} <div class="alert alert-info mt-4"> {__("You are ranked")} <strong>#{$leaderboard.current_user.user_rank}</strong> {__("with")} <strong>{$leaderboard.current_user.user_points}</strong> {__("points")}. {if $leaderboard.next_user} <br> {__("Next rank")}: <strong>{$leaderboard.next_user.user_name}</strong> {__("with")} <strong>{$leaderboard.next_user.user_points}</strong> {__("points")}. <br> {__("Needed to pass")}: <strong>{$leaderboard.next_user.user_points - $leaderboard.current_user.user_points + 1}</strong> {__("points")} {/if} </div> {/if}
<div class="leaderboardp">
{foreach $leaderboard.top_users as $user} <a href="{$system['system_url']}/{$user.user_name}"> <div class="user-rowp {if $user.user_id == $leaderboard.current_user.user_id}highlightp{/if}"> <div class="rankp"> {if $user.user_rank == 1}<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" viewBox="0 0 32 32"><g fill="none"><path fill="#0074ba" d="m18.768 11.51l-5.22-8.58c-.34-.58-.95-.93-1.62-.93h-6.59c-1.45 0-2.36 1.56-1.65 2.82a16.7 16.7 0 0 0 5.43 5.78c.76.59 1.7.91 2.67.91z"/><path fill="#00a6ed" d="M26.658 2h-6.59c-.67 0-1.28.35-1.62.93l-5.22 8.58h6.99c.97 0 1.9-.32 2.67-.91c2.25-1.46 4.11-3.44 5.43-5.78c.7-1.26-.21-2.82-1.66-2.82"/><path fill="#ffb02e" d="M15.99 30c5.545 0 10.04-4.607 10.04-10.29S21.535 9.42 15.99 9.42S5.95 14.027 5.95 19.71S10.445 30 15.99 30"/><path fill="#6d4534" d="M14.076 16.041a1 1 0 0 1 1-1H16a1 1 0 0 1 1 1V23a1 1 0 1 1-2 0v-5.962a1 1 0 0 1-.924-.997"/><path fill="#fcd53f" d="M16 28.76c-2.36 0-4.58-.94-6.24-2.65a9.1 9.1 0 0 1-2.59-6.4c0-2.42.92-4.69 2.59-6.4a8.69 8.69 0 0 1 12.49 0c3.44 3.53 3.44 9.27 0 12.8c-1.68 1.71-3.9 2.65-6.25 2.65m-.01-16.87c-1.95 0-3.91.76-5.39 2.29a7.87 7.87 0 0 0-2.23 5.53c0 2.09.79 4.05 2.23 5.53a7.48 7.48 0 0 0 5.39 2.29c2.04 0 3.95-.81 5.39-2.29c2.97-3.05 2.97-8.01 0-11.06a7.46 7.46 0 0 0-5.39-2.29"/></g></svg> {elseif $user.user_rank == 2}<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" viewBox="0 0 32 32"><g fill="none"><path fill="#0074ba" d="m18.768 11.51l-5.22-8.58c-.34-.58-.95-.93-1.62-.93h-6.59c-1.45 0-2.36 1.56-1.65 2.82a16.7 16.7 0 0 0 5.43 5.78c.76.59 1.7.91 2.67.91z"/><path fill="#00a6ed" d="M26.658 2h-6.59c-.67 0-1.28.35-1.62.93l-5.22 8.58h6.99c.97 0 1.9-.32 2.67-.91c2.25-1.46 4.11-3.44 5.43-5.78c.7-1.26-.21-2.82-1.66-2.82"/><path fill="#bebebe" d="M15.96 30.001c5.545 0 10.04-4.607 10.04-10.29s-4.495-10.29-10.04-10.29s-10.04 4.607-10.04 10.29s4.495 10.29 10.04 10.29"/><path fill="#e6e6e6" d="M15.96 28.761c-2.36 0-4.58-.94-6.24-2.65a9.1 9.1 0 0 1-2.59-6.4c0-2.42.92-4.69 2.59-6.4a8.69 8.69 0 0 1 12.49 0c3.44 3.53 3.44 9.27 0 12.8c-1.68 1.71-3.9 2.65-6.25 2.65m-.01-16.87c-1.95 0-3.91.76-5.39 2.29a7.87 7.87 0 0 0-2.23 5.53c0 2.09.79 4.05 2.23 5.53a7.48 7.48 0 0 0 5.39 2.29c2.04 0 3.95-.81 5.39-2.29c2.97-3.05 2.97-8.01 0-11.06a7.46 7.46 0 0 0-5.39-2.29"/><path fill="#636363" d="M17.838 23.95h-3.97a1 1 0 0 1-.91-.58a1 1 0 0 1 .13-1.07l3.3-4.05c.26-.32.14-.66.1-.76a.64.64 0 0 0-.58-.4h-.05c-.32 0-.61.15-.8.41c-.32.45-.95.56-1.41.24a1.01 1.01 0 0 1-.24-1.41c.56-.79 1.47-1.26 2.44-1.26h.13c1.02.05 1.91.66 2.33 1.59c.43.96.29 2.05-.37 2.86l-1.95 2.4h1.84c.56 0 1.01.45 1.01 1.01s-.44 1.02-1 1.02"/></g></svg> {elseif $user.user_rank == 3}<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" viewBox="0 0 32 32"><g fill="none"><path fill="#0074ba" d="m18.768 11.51l-5.22-8.58c-.34-.58-.95-.93-1.62-.93h-6.59c-1.45 0-2.36 1.56-1.65 2.82a16.7 16.7 0 0 0 5.43 5.78c.76.59 1.7.91 2.67.91z"/><path fill="#00a6ed" d="M26.658 2h-6.59c-.67 0-1.28.35-1.62.93l-5.22 8.58h6.99c.97 0 1.9-.32 2.67-.91c2.25-1.46 4.11-3.44 5.43-5.78c.7-1.26-.21-2.82-1.66-2.82"/><path fill="#d3883e" d="M15.99 30c5.545 0 10.04-4.607 10.04-10.29S21.535 9.42 15.99 9.42S5.95 14.027 5.95 19.71S10.445 30 15.99 30"/><path fill="#f3ad61" d="M16 28.76c-2.36 0-4.58-.94-6.24-2.65a9.1 9.1 0 0 1-2.59-6.4c0-2.42.92-4.69 2.59-6.4a8.69 8.69 0 0 1 12.49 0c3.44 3.53 3.44 9.27 0 12.8c-1.68 1.71-3.9 2.65-6.25 2.65m-.01-16.87c-1.95 0-3.91.76-5.39 2.29a7.87 7.87 0 0 0-2.23 5.53c0 2.09.79 4.05 2.23 5.53a7.48 7.48 0 0 0 5.39 2.29c2.04 0 3.95-.81 5.39-2.29c2.97-3.05 2.97-8.01 0-11.06a7.46 7.46 0 0 0-5.39-2.29"/><path fill="#402a32" d="m17.402 18.275l1.048-1.81a.98.98 0 0 0 0-.977a.97.97 0 0 0-.844-.488h-3.092a.977.977 0 0 0 0 1.953h1.403l-.752 1.311a.96.96 0 0 0-.214.6c0 .54.438.977.977.977c.61 0 1.108.498 1.108 1.108s-.498 1.109-1.108 1.109c-.468 0-.885-.295-1.038-.733a.975.975 0 0 0-1.24-.59a.966.966 0 0 0-.59 1.241A3.06 3.06 0 0 0 15.928 24a3.053 3.053 0 0 0 3.05-3.05a3.07 3.07 0 0 0-1.576-2.675"/></g></svg> {elseif $user.user_rank >= 4} {$user.user_rank} {/if} </div> <div class="user-infop"> <div class="usernamep"> {if $user.user_id == $leaderboard.current_user.user_id} <span class="badge bg-primary ms-1">{__("YOU ARE HERE")}</span> {else} {$user.user_name} {/if} </div> <div class="badgesp"> {foreach $user.badges as $badge} {if $badge.unlocked_level} <img src="{$system['system_url']}/content/uploads/leaderboard/{$badge.key}_{$badge.unlocked_level}.png" class="badge-iconpp" width="20" height="20" /> {/if} {/foreach} </div> </div> <div class="pointsp">{$user.user_points}</div> </div> </a> {/foreach}
</div>
style.css
/* LEADERBOARD PUNCTE */.leaderboardp { max-width: 800px; margin: 0 auto; border-radius: 10px; overflow: hidden;}.rankp,.pointsp { display: flex; flex-direction: column; justify-content: center;}.user-rowp { box-sizing: border-box; display: flex; justify-content: space-between; align-items: center; /* <- This is the key change */ background: #fff; padding: 15px 20px; border-bottom: 1px solid #ddd; transition: background 0.3s;}body.night-mode .user-rowp { background: #212121; border-bottom: 1px solid #485259;}.user-rowp.highlightp { position: relative; background: #ffe9f0;}body.night-mode .user-rowp.highlightp { background: #1D1D1D;}.user-rowp.highlightp::before { content: ""; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background-color: #ff3399;}.rankp { font-weight: bold; min-width: 40px; color: #1D1D1D; font-size: 1.3rem;}body.night-mode .rankp { color: #f8f8f8;}.user-infop { flex-grow: 1; padding: 0 20px;}.usernamep { font-size: 1.1em; font-weight: bold; margin-bottom: 5px;}.badgesp { display: flex; flex-wrap: wrap; gap: 5px;}.badgep { font-size: 0.8em; padding: 3px 8px; border-radius: 12px; background: #ccc; color: #fff;}.badgep.unlockedp { background-color: #28a745;}.badgep.lockedp { background-color: #6c757d;}.pointsp { min-width: 100px; text-align: right; font-weight: bold; color: #28a745; font-size: 1.4rem;}.badge.lockedpp { opacity: 0.5;}.badgepp { padding: 5px; border-radius: 8px; display: flex; flex-direction: column; align-items: start; margin-bottom: 10px;}.badge-iconpp { display: inline-block; vertical-align: middle; width: 32px; height: 32px;}.locked { background-color: #e9ecef;}.badge-container { border: 1px solid #ddd; border-radius: 8px; padding: 15px; margin-bottom: 20px; background: #fff; display: flex; gap: 15px;}body.night-mode .badge-container { background: #212121; border: 1px solid #485259;}.badge-iconpp { flex-shrink: 0;}.badge-info { flex-grow: 1;}.badge-level-entry { margin-top: 5px;}.badge-level-entry.unlocked { color: #28a745; font-weight: bold;}.badge-level-entry.locked { color: #6c757d;}