From 335a52be370f2bc947544281f5fbdd6dffd94ea0 Mon Sep 17 00:00:00 2001 From: Rory Devitt Date: Wed, 10 Sep 2025 14:03:48 +0100 Subject: [PATCH 1/4] Add a tabbed component to display the leaderboard --- sweepy-ui/src/components/LeaderboardTable.jsx | 5 ++ ...eepstakeTable.jsx => ParticipantTable.jsx} | 4 +- sweepy-ui/src/components/SweepstakeDetail.jsx | 4 +- .../src/components/SweepstakesTableTabbed.jsx | 52 +++++++++++++++++++ 4 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 sweepy-ui/src/components/LeaderboardTable.jsx rename sweepy-ui/src/components/{SweepstakeTable.jsx => ParticipantTable.jsx} (97%) create mode 100644 sweepy-ui/src/components/SweepstakesTableTabbed.jsx diff --git a/sweepy-ui/src/components/LeaderboardTable.jsx b/sweepy-ui/src/components/LeaderboardTable.jsx new file mode 100644 index 0000000..cc21fe1 --- /dev/null +++ b/sweepy-ui/src/components/LeaderboardTable.jsx @@ -0,0 +1,5 @@ +function LeaderboardTable() { + return
Content for Leaderboard Tab
; +} + +export default LeaderboardTable; diff --git a/sweepy-ui/src/components/SweepstakeTable.jsx b/sweepy-ui/src/components/ParticipantTable.jsx similarity index 97% rename from sweepy-ui/src/components/SweepstakeTable.jsx rename to sweepy-ui/src/components/ParticipantTable.jsx index 06f6b82..7bf71db 100644 --- a/sweepy-ui/src/components/SweepstakeTable.jsx +++ b/sweepy-ui/src/components/ParticipantTable.jsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; -function SweepstakeTable({ data }) { +function ParticipantTable({ data }) { const [expandedRow, setExpandedRow] = useState(null); // Track expanded row const toggleRow = (index) => { @@ -86,4 +86,4 @@ function SweepstakeTable({ data }) { ); } -export default SweepstakeTable; +export default ParticipantTable; diff --git a/sweepy-ui/src/components/SweepstakeDetail.jsx b/sweepy-ui/src/components/SweepstakeDetail.jsx index 0c5b867..88152cf 100644 --- a/sweepy-ui/src/components/SweepstakeDetail.jsx +++ b/sweepy-ui/src/components/SweepstakeDetail.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import SweepstakeTable from "./SweepstakeTable"; +import SweepstakeTableTabbed from "./SweepstakesTableTabbed"; import SweepstakeHistoryChart from "./SweepstakeHistoryChart"; const SECONDS_IN_MINUTE = 60; @@ -103,7 +103,7 @@ function SweepstakeDetail({

Last refresh: {timeAgo(sweepstake.updated_at)}

- + ); diff --git a/sweepy-ui/src/components/SweepstakesTableTabbed.jsx b/sweepy-ui/src/components/SweepstakesTableTabbed.jsx new file mode 100644 index 0000000..832f90e --- /dev/null +++ b/sweepy-ui/src/components/SweepstakesTableTabbed.jsx @@ -0,0 +1,52 @@ +import React, { useState } from "react"; + +import ParticipantTable from "./ParticipantTable"; +import LeaderboardTable from "./LeaderboardTable"; + +const SweepstakeTableTabbed = ({ data }) => { + const [activeTab, setActiveTab] = useState("table"); + + const renderContent = () => { + switch (activeTab) { + case "participant": + return ; + case "leaderboard": + return ; + default: + return null; + } + }; + + return ( +
+ {/* Tab Buttons */} +
+ + +
+ + {/* Tab Content */} +
{renderContent()}
+
+ ); +}; + +export default SweepstakeTableTabbed; From cc08d1ceb2748d77ceec268366c18ad2b8752f76 Mon Sep 17 00:00:00 2001 From: Rory Devitt Date: Thu, 11 Sep 2025 10:32:32 +0100 Subject: [PATCH 2/4] Refactor sweepstakes page to tabbed views --- sweepy-ui/src/components/LeaderboardTable.jsx | 2 +- sweepy-ui/src/components/SweepstakeDetail.jsx | 7 ++++--- .../src/components/SweepstakesTableTabbed.jsx | 21 +++++++++++++++---- sweepy/generate_sweepstakes.py | 2 +- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/sweepy-ui/src/components/LeaderboardTable.jsx b/sweepy-ui/src/components/LeaderboardTable.jsx index cc21fe1..d0b9053 100644 --- a/sweepy-ui/src/components/LeaderboardTable.jsx +++ b/sweepy-ui/src/components/LeaderboardTable.jsx @@ -1,5 +1,5 @@ function LeaderboardTable() { - return
Content for Leaderboard Tab
; + return
Coming Soon!
; } export default LeaderboardTable; diff --git a/sweepy-ui/src/components/SweepstakeDetail.jsx b/sweepy-ui/src/components/SweepstakeDetail.jsx index 88152cf..17a6af5 100644 --- a/sweepy-ui/src/components/SweepstakeDetail.jsx +++ b/sweepy-ui/src/components/SweepstakeDetail.jsx @@ -1,6 +1,5 @@ import { useState, useEffect } from "react"; import SweepstakeTableTabbed from "./SweepstakesTableTabbed"; -import SweepstakeHistoryChart from "./SweepstakeHistoryChart"; const SECONDS_IN_MINUTE = 60; const SECONDS_IN_HOUR = 3600; @@ -103,8 +102,10 @@ function SweepstakeDetail({

Last refresh: {timeAgo(sweepstake.updated_at)}

- - + ); } diff --git a/sweepy-ui/src/components/SweepstakesTableTabbed.jsx b/sweepy-ui/src/components/SweepstakesTableTabbed.jsx index 832f90e..968e770 100644 --- a/sweepy-ui/src/components/SweepstakesTableTabbed.jsx +++ b/sweepy-ui/src/components/SweepstakesTableTabbed.jsx @@ -2,16 +2,19 @@ import React, { useState } from "react"; import ParticipantTable from "./ParticipantTable"; import LeaderboardTable from "./LeaderboardTable"; +import SweepstakeHistoryChart from "./SweepstakeHistoryChart"; -const SweepstakeTableTabbed = ({ data }) => { - const [activeTab, setActiveTab] = useState("table"); +const SweepstakeTableTabbed = ({ sweepstake, history }) => { + const [activeTab, setActiveTab] = useState("participant"); // Default to participant tab const renderContent = () => { switch (activeTab) { case "participant": - return ; + return ; + case "history": + return ; case "leaderboard": - return ; + return ; default: return null; } @@ -31,6 +34,16 @@ const SweepstakeTableTabbed = ({ data }) => { > Participants + - + {sweepstake.tournament_id && ( + + )} {/* Tab Content */} From c81ffb219c36ad7bc7f59ef8797dc45d61a18770 Mon Sep 17 00:00:00 2001 From: Rory Devitt Date: Fri, 12 Sep 2025 09:17:08 +0100 Subject: [PATCH 4/4] Add sortable leaderboard table to tabbed sweepstake view --- sweepy-ui/src/components/LeaderboardTable.jsx | 38 +++++++++- sweepy-ui/src/components/ParticipantTable.jsx | 19 +---- sweepy-ui/src/components/SortableTable.jsx | 73 +++++++++++++++++++ sweepy-ui/src/components/SweepstakeDetail.jsx | 2 +- .../src/components/SweepstakesTableTabbed.jsx | 2 +- sweepy-ui/src/utils/format.js | 19 +++++ sweepy/models/sweepstakes.py | 2 +- 7 files changed, 134 insertions(+), 21 deletions(-) create mode 100644 sweepy-ui/src/components/SortableTable.jsx create mode 100644 sweepy-ui/src/utils/format.js diff --git a/sweepy-ui/src/components/LeaderboardTable.jsx b/sweepy-ui/src/components/LeaderboardTable.jsx index d0b9053..b4d6e6a 100644 --- a/sweepy-ui/src/components/LeaderboardTable.jsx +++ b/sweepy-ui/src/components/LeaderboardTable.jsx @@ -1,5 +1,39 @@ -function LeaderboardTable() { - return
Coming Soon!
; +import { percentifyProbability, stringifyScore } from "../utils/format"; +import SortableTable from "./SortableTable"; + +function LeaderboardTable({ data }) { + const allAssignments = data.participants + .flatMap((p) => + p.assignments.map((a) => ({ + probability: a.implied_probability, + player: a.name, + score: a.score, + participant: p.name, + })), + ) + .sort((a, b) => a.score - b.score); + + const columns = { + participant: true, + player: true, + probability: true, + score: data.tournament_id ? true : false, + }; + + const columnFormatters = { + probability: (val) => percentifyProbability(val), + score: (val) => stringifyScore(val), + }; + + console.log("Leaderboard data:", allAssignments); + + return ( + + ); } export default LeaderboardTable; diff --git a/sweepy-ui/src/components/ParticipantTable.jsx b/sweepy-ui/src/components/ParticipantTable.jsx index 7bf71db..808487f 100644 --- a/sweepy-ui/src/components/ParticipantTable.jsx +++ b/sweepy-ui/src/components/ParticipantTable.jsx @@ -1,24 +1,13 @@ import React, { useState } from "react"; +import { stringifyScore, percentifyProbability } from "../utils/format"; + function ParticipantTable({ data }) { const [expandedRow, setExpandedRow] = useState(null); // Track expanded row const toggleRow = (index) => { setExpandedRow(expandedRow === index ? null : index); }; - - const stringifyScore = (score) => { - if (score === null || score === undefined) { - return "-"; - } else if (score == 0) { - return "E"; - } else if (score > 0) { - return `+${score}`; - } else { - return `${score}`; - } - }; - const sortedParticipants = [...data.participants].sort((a, b) => { return parseFloat(b.equity) - parseFloat(a.equity); }); @@ -37,9 +26,7 @@ function ParticipantTable({ data }) { toggleRow(index)}> {p.name} - - {(parseFloat(p.equity) * 100).toFixed(2)}% - + {percentifyProbability(p.equity)} {/* Accordion for the expanded row */} diff --git a/sweepy-ui/src/components/SortableTable.jsx b/sweepy-ui/src/components/SortableTable.jsx new file mode 100644 index 0000000..63ae805 --- /dev/null +++ b/sweepy-ui/src/components/SortableTable.jsx @@ -0,0 +1,73 @@ +import React, { useState } from "react"; + +const SortableTable = ({ sortableColumns, data, columnFormatters }) => { + // columns = { name: true, score: true, probability: false } + const [sortConfig, setSortConfig] = useState({ key: null, direction: "asc" }); + + const sortedData = React.useMemo(() => { + let sortableData = [...data]; + if (sortConfig.key !== null && sortableColumns[sortConfig.key]) { + sortableData.sort((a, b) => { + if (a[sortConfig.key] < b[sortConfig.key]) { + return sortConfig.direction === "asc" ? -1 : 1; + } + if (a[sortConfig.key] > b[sortConfig.key]) { + return sortConfig.direction === "asc" ? 1 : -1; + } + return 0; + }); + } + return sortableData; + }, [data, sortConfig, sortableColumns]); + + const requestSort = (key) => { + if (!sortableColumns[key]) return; // ignore clicks if column not sortable + let direction = "asc"; + if (sortConfig.key === key && sortConfig.direction === "asc") { + direction = "desc"; + } + setSortConfig({ key, direction }); + }; + + const getSortArrow = (key) => { + if (!sortableColumns[key]) return ""; // no arrow if not sortable + if (sortConfig.key !== key) return "↕"; + return sortConfig.direction === "asc" ? "↑" : "↓"; + }; + + return ( + + + + {Object.keys(sortableColumns).map((colKey) => ( + + ))} + + + + {sortedData.map((row, idx) => ( + + {Object.keys(sortableColumns).map((colKey) => ( + + ))} + + ))} + +
requestSort(colKey)} + > + {colKey.charAt(0).toUpperCase() + colKey.slice(1)}{" "} + {getSortArrow(colKey)} +
+ {columnFormatters[colKey] + ? columnFormatters[colKey](row[colKey]) + : row[colKey]} +
+ ); +}; + +export default SortableTable; diff --git a/sweepy-ui/src/components/SweepstakeDetail.jsx b/sweepy-ui/src/components/SweepstakeDetail.jsx index 17a6af5..7e81d35 100644 --- a/sweepy-ui/src/components/SweepstakeDetail.jsx +++ b/sweepy-ui/src/components/SweepstakeDetail.jsx @@ -83,7 +83,7 @@ function SweepstakeDetail({ } return ( -
+

{sweepstake.name} (ID: {sweepstake.id})

diff --git a/sweepy-ui/src/components/SweepstakesTableTabbed.jsx b/sweepy-ui/src/components/SweepstakesTableTabbed.jsx index b0e013d..6b65da1 100644 --- a/sweepy-ui/src/components/SweepstakesTableTabbed.jsx +++ b/sweepy-ui/src/components/SweepstakesTableTabbed.jsx @@ -21,7 +21,7 @@ const SweepstakeTableTabbed = ({ sweepstake, history }) => { }; return ( -
+
{/* Tab Buttons */}