diff --git a/sweepy-ui/src/components/LeaderboardTable.jsx b/sweepy-ui/src/components/LeaderboardTable.jsx new file mode 100644 index 0000000..b4d6e6a --- /dev/null +++ b/sweepy-ui/src/components/LeaderboardTable.jsx @@ -0,0 +1,39 @@ +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/SweepstakeTable.jsx b/sweepy-ui/src/components/ParticipantTable.jsx similarity index 85% rename from sweepy-ui/src/components/SweepstakeTable.jsx rename to sweepy-ui/src/components/ParticipantTable.jsx index 06f6b82..808487f 100644 --- a/sweepy-ui/src/components/SweepstakeTable.jsx +++ b/sweepy-ui/src/components/ParticipantTable.jsx @@ -1,24 +1,13 @@ import React, { useState } from "react"; -function SweepstakeTable({ data }) { +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 SweepstakeTable({ data }) { toggleRow(index)}> {p.name} - - {(parseFloat(p.equity) * 100).toFixed(2)}% - + {percentifyProbability(p.equity)} {/* Accordion for the expanded row */} @@ -86,4 +73,4 @@ function SweepstakeTable({ data }) { ); } -export default SweepstakeTable; +export default ParticipantTable; 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 0c5b867..7e81d35 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 SweepstakeTable from "./SweepstakeTable"; -import SweepstakeHistoryChart from "./SweepstakeHistoryChart"; +import SweepstakeTableTabbed from "./SweepstakesTableTabbed"; const SECONDS_IN_MINUTE = 60; const SECONDS_IN_HOUR = 3600; @@ -84,7 +83,7 @@ function SweepstakeDetail({ } return ( -
+

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

@@ -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 new file mode 100644 index 0000000..6b65da1 --- /dev/null +++ b/sweepy-ui/src/components/SweepstakesTableTabbed.jsx @@ -0,0 +1,67 @@ +import React, { useState } from "react"; + +import ParticipantTable from "./ParticipantTable"; +import LeaderboardTable from "./LeaderboardTable"; +import SweepstakeHistoryChart from "./SweepstakeHistoryChart"; + +const SweepstakeTableTabbed = ({ sweepstake, history }) => { + const [activeTab, setActiveTab] = useState("participant"); // Default to participant tab + + const renderContent = () => { + switch (activeTab) { + case "participant": + return ; + case "history": + return ; + case "leaderboard": + return ; + default: + return null; + } + }; + + return ( +
+ {/* Tab Buttons */} +
+ + + {sweepstake.tournament_id && ( + + )} +
+ + {/* Tab Content */} +
{renderContent()}
+
+ ); +}; + +export default SweepstakeTableTabbed; diff --git a/sweepy-ui/src/utils/format.js b/sweepy-ui/src/utils/format.js new file mode 100644 index 0000000..fb4a3ee --- /dev/null +++ b/sweepy-ui/src/utils/format.js @@ -0,0 +1,19 @@ +export function stringifyScore(score) { + if (score === null || score === undefined) { + return "-"; + } else if (score === 0) { + return "E"; + } else if (score > 0) { + return `+${score}`; + } else { + return `${score}`; + } +} + +export function percentifyProbability(prob) { + if (prob === null || prob === undefined) { + return "-"; + } else { + return `${(parseFloat(prob) * 100).toFixed(2)}%`; + } +} diff --git a/sweepy/generate_sweepstakes.py b/sweepy/generate_sweepstakes.py index aa35a41..ee0dae6 100644 --- a/sweepy/generate_sweepstakes.py +++ b/sweepy/generate_sweepstakes.py @@ -202,7 +202,7 @@ def refresh_sweepstake_odds( Refresh the sweepstake by re-fetching the market data and updating the participants. """ - latest_data = get_selections(client, sweepstake_db.market_id, False) + latest_data = get_selections(client, sweepstake_db.market_id) fetched_at = datetime.datetime.now(datetime.timezone.utc) if not latest_data: raise MarketNotFoundException( diff --git a/sweepy/models/sweepstakes.py b/sweepy/models/sweepstakes.py index aa9d874..9fb9e8c 100644 --- a/sweepy/models/sweepstakes.py +++ b/sweepy/models/sweepstakes.py @@ -9,7 +9,7 @@ class RunnerOdds(BaseModel): provider_id: str name: str implied_probability: Decimal = condecimal(ge=0, le=1) - score: Decimal | None = None + score: int | None = None def __le__(self, other: "RunnerOdds") -> bool: return self.implied_probability <= other.implied_probability