This repository has no description
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;