This repository has no description
0

Configure Feed

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

at main 15 kB View raw
1// src/components/ScoreResult.js 2 3import React, { useState, useRef, useEffect, useCallback } from "react"; 4import { 5 BarChart, 6 Bar, 7 XAxis, 8 YAxis, 9 Tooltip, 10 Legend, 11 ResponsiveContainer, 12} from "recharts"; 13import PropTypes from "prop-types"; // Import PropTypes for type checking 14import "./ScoreResult.css"; 15 16const ScoreResult = ({ result, loading }) => { 17 // State Hooks 18 const [showBluesky, setShowBluesky] = useState(true); 19 const [showAtproto, setShowAtproto] = useState(true); 20 const [, setActiveHandle] = useState(null); 21 22 // Ref to track if the component is mounted 23 const isMounted = useRef(true); 24 25 useEffect(() => { 26 // Cleanup function to set isMounted to false when component unmounts 27 return () => { 28 isMounted.current = false; 29 }; 30 }, []); 31 32 // Safe state updater to prevent setting state on unmounted components 33 const safeSetActiveHandle = useCallback((handle) => { 34 if (isMounted.current && typeof setActiveHandle === "function") { 35 setActiveHandle(handle); 36 } 37 }, []); 38 39 // Debugging: Log the received result prop 40 useEffect(() => { 41 console.log("ScoreResult received:", result); 42 }, [result]); 43 44 if (loading) { 45 return ( 46 <div className="score-result"> 47 {/* Loading Skeletons */} 48 <div className="loading-skeleton"> 49 <div className="skeleton-title"></div> 50 <div className="skeleton-bar"></div> 51 <div className="skeleton-paragraph"></div> 52 <div className="skeleton-paragraph"></div> 53 <div className="skeleton-paragraph"></div> 54 </div> 55 <div className="loading-skeleton"> 56 <div className="skeleton-title"></div> 57 <div className="skeleton-bar"></div> 58 <div className="skeleton-paragraph"></div> 59 <div className="skeleton-paragraph"></div> 60 <div className="skeleton-paragraph"></div> 61 </div> 62 </div> 63 ); 64 } 65 66 if (!result) { 67 return null; 68 } 69 70 if (result.error) { 71 return <div className="error">Error: {result.error}</div>; 72 } 73 74 /** 75 * Determine if result is array or single object. 76 */ 77 const isComparison = Array.isArray(result); 78 79 /** 80 * Function to extract score data and breakdown from a single score object 81 * @param {Object} scoreObj - Single score object. 82 * @returns {Object} - { scoreData, breakdownData } 83 */ 84 const extractData = (scoreObj) => { 85 // If the scoreObj has 'data' and 'breakdown', use them 86 if (scoreObj.data && scoreObj.breakdown) { 87 return { 88 scoreData: scoreObj.data, 89 breakdownData: scoreObj.breakdown, 90 }; 91 } 92 // Else, assume scoreObj itself has data fields and breakdown 93 return { 94 scoreData: scoreObj, 95 breakdownData: scoreObj.breakdown || {}, 96 }; 97 }; 98 99 /** 100 * Renders the score breakdown sections for a given identity. 101 * @param {Object} scoreObj - The result data for a single identity. 102 * @param {String} label - A unique label to prevent key conflicts. 103 */ 104 const renderScoreBreakdown = (scoreObj, label) => { 105 const { scoreData, breakdownData } = extractData(scoreObj); 106 107 // Directly extract handle from scoreData 108 const { handle = "Unknown Handle" } = scoreData; 109 110 // Safeguard against missing breakdown sections 111 const blueskyBreakdown = breakdownData.bluesky || []; 112 const atprotoBreakdown = breakdownData.atproto || []; 113 114 return ( 115 <div className="score-breakdown" key={label}> 116 {/* Username Header */} 117 <h3 className="score-breakdown-header">{handle}</h3> 118 119 {/* Bluesky Score Breakdown */} 120 <div className="breakdown-section"> 121 <h4>Bluesky Score Breakdown</h4> 122 {blueskyBreakdown.length > 0 ? ( 123 <ul> 124 {blueskyBreakdown.map((item, index) => ( 125 <li key={`${label}-bluesky-${index}`}> 126 {item.description}: {Math.ceil(item.points)} points 127 {(item.value || item.details) && ( 128 <span> 129 {" "} 130 ( 131 {item.value 132 ? item.value 133 : item.details 134 ? item.details 135 : ""} 136 ) 137 </span> 138 )} 139 </li> 140 ))} 141 </ul> 142 ) : ( 143 <p>No Bluesky score breakdown available.</p> 144 )} 145 </div> 146 147 {/* Atproto Score Breakdown */} 148 <div className="breakdown-section"> 149 <h4>AT Proto Score Breakdown</h4> 150 {atprotoBreakdown.length > 0 ? ( 151 <ul> 152 {atprotoBreakdown.map((item, index) => ( 153 <li key={`${label}-atproto-${index}`}> 154 {item.description}: {Math.ceil(item.points)} points 155 {(item.value || item.details) && ( 156 <span> 157 {" "} 158 ( 159 {item.value 160 ? item.value 161 : item.details 162 ? item.details 163 : ""} 164 ) 165 </span> 166 )} 167 </li> 168 ))} 169 </ul> 170 ) : ( 171 <p>No AT Proto score breakdown available.</p> 172 )} 173 </div> 174 </div> 175 ); 176 }; 177 178 /** 179 * Renders the score toggle checkboxes. 180 */ 181 const renderCheckboxes = () => ( 182 <div className="score-toggle-controls"> 183 <label> 184 <input 185 type="checkbox" 186 checked={showBluesky} 187 onChange={() => setShowBluesky((prev) => !prev)} 188 /> 189 Show Bluesky Score 190 </label> 191 <label> 192 <input 193 type="checkbox" 194 checked={showAtproto} 195 onChange={() => setShowAtproto((prev) => !prev)} 196 /> 197 Show AT Proto Score 198 </label> 199 </div> 200 ); 201 202 /** 203 * Custom Tooltip Component for Recharts 204 */ 205 const CustomTooltip = ({ active, payload, label }) => { 206 if (active && payload && payload.length) { 207 const dataPoint = payload[0].payload; // Assuming first payload contains necessary data 208 return ( 209 <div className="custom-tooltip"> 210 <p className="label"> 211 <strong>{dataPoint.handle || "Unknown Handle"}</strong> 212 </p> 213 {showBluesky && ( 214 <p>{`Bluesky Score: ${dataPoint.Bluesky || 0}`}</p> 215 )} 216 {showAtproto && ( 217 <p>{`AT Proto Score: ${dataPoint.Atproto || 0}`}</p> 218 )} 219 <p>{`Combined Score: ${dataPoint.Combined || 0}`}</p> 220 </div> 221 ); 222 } 223 return null; 224 }; 225 226 CustomTooltip.propTypes = { 227 active: PropTypes.bool, 228 payload: PropTypes.array, 229 label: PropTypes.string, 230 }; 231 232 /** 233 * Renders the score chart for single mode. 234 * @param {Object} scoreObj - The result data for a single identity. 235 */ 236 const renderSingleChart = (scoreObj) => { 237 const { scoreData } = extractData(scoreObj); 238 239 const { handle = "Unknown Handle", blueskyScore, atprotoScore, combinedScore, generatedAt } = scoreData; 240 241 // Prepare data for Recharts 242 const chartData = [ 243 { 244 handle: handle, 245 Bluesky: blueskyScore || 0, 246 Atproto: atprotoScore || 0, 247 Combined: combinedScore || 0, 248 generatedAt: generatedAt || new Date().toISOString(), 249 }, 250 ]; 251 252 console.log("Single Chart Data:", chartData); 253 254 return ( 255 <div className="single-chart"> 256 <h3>Score Overview</h3> 257 {/* Render Checkboxes */} 258 {renderCheckboxes()} 259 <ResponsiveContainer width="100%" height={300}> 260 <BarChart 261 data={chartData} 262 margin={{ top: 20, right: 30, left: 40, bottom: 20 }} 263 onMouseMove={(state) => { 264 if (state.isTooltipActive && state.activeLabel) { 265 safeSetActiveHandle(state.activeLabel); 266 } else { 267 safeSetActiveHandle(null); 268 } 269 }} 270 onMouseLeave={() => safeSetActiveHandle(null)} 271 > 272 <XAxis dataKey="handle" /> 273 <YAxis /> 274 <Tooltip content={<CustomTooltip />} /> {/* Use Custom Tooltip */} 275 <Legend /> 276 {showBluesky && ( 277 <Bar dataKey="Bluesky" stackId="a" fill="#3B9AF8" /> 278 )} 279 {showAtproto && ( 280 <Bar dataKey="Atproto" stackId="a" fill="#28a745" /> 281 )} 282 </BarChart> 283 </ResponsiveContainer> 284 </div> 285 ); 286 }; 287 288 /** 289 * Renders the score chart for comparison mode. 290 */ 291 const renderComparisonChart = () => { 292 if (isComparison && Array.isArray(result) && result.length === 2) { 293 // Prepare data for Recharts 294 const chartData = result.map((scoreObj) => { 295 const { scoreData } = extractData(scoreObj); 296 const { handle = "Unknown Handle", blueskyScore, atprotoScore, combinedScore, generatedAt } = scoreData; 297 298 return { 299 handle: handle, 300 Bluesky: blueskyScore || 0, 301 Atproto: atprotoScore || 0, 302 Combined: combinedScore || 0, 303 generatedAt: generatedAt || new Date().toISOString(), 304 }; 305 }); 306 307 console.log("Comparison Chart Data:", chartData); 308 309 return ( 310 <div className="comparison-chart"> 311 <h3>Score Comparison</h3> 312 {/* Render Checkboxes */} 313 {renderCheckboxes()} 314 <ResponsiveContainer width="100%" height={300}> 315 <BarChart 316 data={chartData} 317 margin={{ top: 20, right: 30, left: 40, bottom: 20 }} 318 onMouseMove={(state) => { 319 if (state.isTooltipActive && state.activeLabel) { 320 safeSetActiveHandle(state.activeLabel); 321 } else { 322 safeSetActiveHandle(null); 323 } 324 }} 325 onMouseLeave={() => safeSetActiveHandle(null)} 326 > 327 <XAxis dataKey="handle" /> 328 <YAxis /> 329 <Tooltip content={<CustomTooltip />} /> {/* Use Custom Tooltip */} 330 <Legend /> 331 {showBluesky && ( 332 <Bar dataKey="Bluesky" stackId="a" fill="#3B9AF8" /> 333 )} 334 {showAtproto && ( 335 <Bar dataKey="Atproto" stackId="a" fill="#28a745" /> 336 )} 337 </BarChart> 338 </ResponsiveContainer> 339 </div> 340 ); 341 } 342 return null; 343 }; 344 345 /** 346 * Renders the comparison summary. 347 */ 348 const renderComparisonSummary = () => { 349 if (isComparison && Array.isArray(result) && result.length === 2) { 350 const [first, second] = result.map((scoreObj) => extractData(scoreObj).scoreData); 351 352 const firstScore = first.combinedScore || 0; 353 const secondScore = second.combinedScore || 0; 354 355 return ( 356 <div className="comparison-summary"> 357 <div className="comparison-summary-text"> 358 <h3>High-Level Comparison</h3> 359 <p> 360 <strong>{first.handle || "Unknown Handle"}</strong> has a 361 combined score of <strong>{Math.ceil(firstScore.toFixed(2))}</strong>. 362 </p> 363 <p> 364 <strong>{second.handle || "Unknown Handle"}</strong> has a 365 combined score of <strong>{Math.ceil(secondScore.toFixed(2))}</strong>. 366 </p> 367 <p> 368 {firstScore > secondScore ? ( 369 <span> 370 <strong>{first.handle || "First User"}</strong> is 371 ranked higher. 372 </span> 373 ) : firstScore < secondScore ? ( 374 <span> 375 <strong>{second.handle || "Second User"}</strong> is 376 ranked higher. 377 </span> 378 ) : ( 379 <span>Both scores are equal!</span> 380 )} 381 </p> 382 </div> 383 </div> 384 ); 385 } 386 return null; 387 }; 388 389 /** 390 * Renders the score chart (single or comparison mode). 391 */ 392 const renderChart = () => { 393 if (isComparison && Array.isArray(result)) { 394 return ( 395 <> 396 {renderComparisonChart()} 397 {renderComparisonSummary()} 398 </> 399 ); 400 } else { 401 return renderSingleChart(result); 402 } 403 }; 404 405 /** 406 * Renders the score breakdowns for all identities. 407 */ 408 const renderAllBreakdowns = () => { 409 if (isComparison && Array.isArray(result)) { 410 return result.map((scoreObj, index) => 411 renderScoreBreakdown(scoreObj, `comparison-${index}`) 412 ); 413 } else { 414 return renderScoreBreakdown(result, "single"); 415 } 416 }; 417 418 return ( 419 <div 420 className={`score-result ${isComparison ? "comparison-mode" : ""}`} 421 > 422 {renderChart()} 423 <div className="score-breakdowns">{renderAllBreakdowns()}</div> 424 </div> 425 ); 426}; 427 428// Define PropTypes for type checking 429ScoreResult.propTypes = { 430 result: PropTypes.oneOfType([ 431 // Comparison Mode: Array of score objects 432 PropTypes.arrayOf( 433 PropTypes.shape({ 434 handle: PropTypes.string.isRequired, 435 did: PropTypes.string.isRequired, 436 blueskyScore: PropTypes.number.isRequired, 437 atprotoScore: PropTypes.number.isRequired, 438 combinedScore: PropTypes.number.isRequired, 439 generatedAt: PropTypes.string.isRequired, 440 breakdown: PropTypes.shape({ 441 bluesky: PropTypes.arrayOf( 442 PropTypes.shape({ 443 description: PropTypes.string.isRequired, 444 points: PropTypes.number.isRequired, 445 value: PropTypes.string, 446 details: PropTypes.string, 447 }) 448 ), 449 atproto: PropTypes.arrayOf( 450 PropTypes.shape({ 451 description: PropTypes.string.isRequired, 452 points: PropTypes.number.isRequired, 453 value: PropTypes.string, 454 details: PropTypes.string, 455 }) 456 ), 457 }).isRequired, 458 }) 459 ), 460 // Single Mode: Single score object 461 PropTypes.shape({ 462 handle: PropTypes.string.isRequired, 463 did: PropTypes.string.isRequired, 464 blueskyScore: PropTypes.number.isRequired, 465 atprotoScore: PropTypes.number.isRequired, 466 combinedScore: PropTypes.number.isRequired, 467 generatedAt: PropTypes.string.isRequired, 468 breakdown: PropTypes.shape({ 469 bluesky: PropTypes.arrayOf( 470 PropTypes.shape({ 471 description: PropTypes.string.isRequired, 472 points: PropTypes.number.isRequired, 473 value: PropTypes.string, 474 details: PropTypes.string, 475 }) 476 ), 477 atproto: PropTypes.arrayOf( 478 PropTypes.shape({ 479 description: PropTypes.string.isRequired, 480 points: PropTypes.number.isRequired, 481 value: PropTypes.string, 482 details: PropTypes.string, 483 }) 484 ), 485 }).isRequired, 486 }), 487 ]), 488 loading: PropTypes.bool.isRequired, 489}; 490 491export default ScoreResult;