This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 8.0 kB View raw
1import React, { useState, useEffect, useCallback } from 'react'; 2import { supabase } from '../../lib/supabase'; 3import { Link } from 'react-router-dom'; 4import './Leaderboard.css'; 5 6// Function to truncate handles that are too long 7const truncateHandle = (handle, maxLength = 20) => { 8 if (!handle) return ''; 9 if (handle.length <= maxLength) return handle; 10 11 // For handles with domains, try to preserve the beginning and domain part 12 if (handle.includes('.')) { 13 const parts = handle.split('.'); 14 const domain = parts.slice(-2).join('.'); 15 const username = parts.slice(0, -2).join('.'); 16 17 // If username + domain is short enough, just return it 18 if (username.length + domain.length + 1 <= maxLength) { 19 return `${username}.${domain}`; 20 } 21 22 // Otherwise, truncate the username part 23 const availableLength = maxLength - domain.length - 4; // Account for ellipsis and dot 24 if (availableLength > 0) { 25 return `${username.substring(0, availableLength)}...${domain}`; 26 } 27 } 28 29 // For simple handles or fallback 30 return `${handle.substring(0, maxLength - 3)}...`; 31}; 32 33const Leaderboard = () => { 34 const [users, setUsers] = useState([]); 35 const [runnerUps, setRunnerUps] = useState([]); 36 const [loading, setLoading] = useState(true); 37 const [error, setError] = useState(null); 38 const [scoreType, setScoreType] = useState('combined_score'); 39 40 const scoreTypes = { 41 combined_score: 'Combined Score', 42 bluesky_score: 'Bluesky Score', 43 atproto_score: 'ATProto Score' 44 }; 45 46 // Calculate protocol balance and determine which side has more activity 47 const calculateProtocolBalance = (bskyRecords, nonBskyRecords) => { 48 if (!bskyRecords && !nonBskyRecords) return { score: 50, leaning: 'neutral' }; 49 const total = bskyRecords + nonBskyRecords; 50 if (total === 0) return { score: 50, leaning: 'neutral' }; 51 52 // Calculate the percentage of bsky records 53 const bskyPercentage = (bskyRecords / total) * 100; 54 55 // Return both the percentage and which side it leans towards 56 return { 57 score: bskyPercentage, 58 leaning: bskyRecords > nonBskyRecords ? 'bsky' : 'atproto' 59 }; 60 }; 61 62 const getBalanceDescription = (bskyRecords, nonBskyRecords) => { 63 const { score, leaning } = calculateProtocolBalance(bskyRecords, nonBskyRecords); 64 65 if (score >= 45 && score <= 55) return 'Balanced usage'; 66 if (leaning === 'bsky') { 67 if (score > 90) return 'Almost entirely Bluesky'; 68 if (score > 75) return 'Heavily Bluesky'; 69 return 'Leans Bluesky'; 70 } else { 71 if (score < 10) return 'Almost entirely ATProto'; 72 if (score < 25) return 'Heavily ATProto'; 73 return 'Leans ATProto'; 74 } 75 }; 76 77 const fetchUsers = useCallback(async () => { 78 try { 79 setLoading(true); 80 81 console.log(`Fetching leaderboard data for scoreType: ${scoreType}`); 82 83 // Call the backend endpoint instead of directly querying Supabase 84 const response = await fetch(`https://api.cred.blue/api/leaderboard?scoreType=${scoreType}&limit=100`); 85 86 // Check for non-200 responses 87 if (!response.ok) { 88 let errorMessage; 89 try { 90 // Try to parse error JSON 91 const errorData = await response.json(); 92 errorMessage = errorData.error || `Server error: ${response.status} ${response.statusText}`; 93 } catch (parseError) { 94 // If JSON parsing fails, use status text 95 errorMessage = `Server error: ${response.status} ${response.statusText}`; 96 } 97 throw new Error(errorMessage); 98 } 99 100 // Parse successful response 101 let data; 102 try { 103 data = await response.json(); 104 } catch (parseError) { 105 console.error('Error parsing JSON response:', parseError); 106 throw new Error('Invalid response format from server'); 107 } 108 109 // Debug logging 110 console.log(`Received ${data.topUsers?.length || 0} top users and ${data.runnerUps?.length || 0} runner ups`); 111 112 setUsers(data.topUsers || []); 113 setRunnerUps(data.runnerUps || []); 114 115 } catch (err) { 116 console.error('Error fetching leaderboard:', err); 117 setError(err.message); 118 } finally { 119 setLoading(false); 120 } 121 }, [scoreType]); 122 123 useEffect(() => { 124 fetchUsers(); 125 }, [fetchUsers]); 126 127 const handleScoreTypeChange = (type) => { 128 setScoreType(type); 129 setUsers([]); 130 setRunnerUps([]); 131 }; 132 133 const renderUserRow = (user, index, isRunnerUp = false) => ( 134 <tr key={user.handle} className={isRunnerUp ? 'runner-up' : ''}> 135 <td className="rank-cell">#{index + 1}</td> 136 <td> 137 <a 138 href={`/${user.handle}`} 139 className="user-handle" 140 title={`@${user.handle}`} // Add title for hover to see full handle 141 > 142 @{truncateHandle(user.handle)} 143 </a> 144 </td> 145 <td className="score-cell"> 146 {Math.round(user[scoreType] || 0)} 147 </td> 148 <td> 149 <div className="balance-indicator"> 150 <div className="balance-track"> 151 <div 152 className="balance-bar" 153 style={{ 154 left: `${calculateProtocolBalance(user.total_bsky_records, user.total_non_bsky_records).score}%` 155 }} 156 ></div> 157 <div className="protocol-labels"> 158 <span className="at-proto-label">ATProto</span> 159 <span className="bsky-label">Bluesky</span> 160 </div> 161 </div> 162 <span className="balance-description"> 163 {getBalanceDescription(user.total_bsky_records, user.total_non_bsky_records)} 164 </span> 165 </div> 166 </td> 167 <td className="age-cell"> 168 {Math.round(user.age_in_days)} days 169 </td> 170 <td> 171 <span className="activity-badge"> 172 {user.activity_status || 'Unknown'} 173 </span> 174 </td> 175 </tr> 176 ); 177 178 return ( 179 <div className="leaderboard-container"> 180 <div className="leaderboard-card"> 181 <div className="leaderboard-header"> 182 <h1>Leaderboard (Top 100)</h1> 183 <p className="leaderboard-description"> 184 Discover the highest scoring accounts across Bluesky and the AT Protocol network that have been calculated so far. Scores are based on numerous factors across activity and protocol participation. If a username has never been searched on cred.blue, it won't appear here. <Link to="/methodology">Learn more about the scoring methodology.</Link> 185 </p> 186 </div> 187 188 <div className="score-type-filters"> 189 {Object.entries(scoreTypes).map(([value, label]) => ( 190 <button 191 key={value} 192 onClick={() => handleScoreTypeChange(value)} 193 className={`filter-button ${scoreType === value ? 'active' : ''}`} 194 > 195 {label} 196 </button> 197 ))} 198 </div> 199 200 {error && ( 201 <div className="error-message"> 202 Error loading leaderboard: {error} 203 </div> 204 )} 205 206 <div className="table-wrapper"> 207 <div className="table-container"> 208 <table className="leaderboard-table"> 209 <thead> 210 <tr> 211 <th>Rank</th> 212 <th>Handle</th> 213 <th className="score-column">Score</th> 214 <th>Protocol Balance</th> 215 <th>Account Age</th> 216 <th>Activity Status</th> 217 </tr> 218 </thead> 219 <tbody> 220 {users.map((user, index) => renderUserRow(user, index))} 221 {runnerUps.map((user, index) => renderUserRow(user, index + 100, true))} 222 </tbody> 223 </table> 224 </div> 225 </div> 226 227 {loading && ( 228 <div className="loading-container"> 229 <div className="loading-spinner"></div> 230 </div> 231 )} 232 </div> 233 </div> 234 ); 235}; 236 237export default Leaderboard;