diff --git a/package-lock.json b/package-lock.json index 0681d76ac0..a1c8b5494f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "@melloware/react-logviewer": "^6.6.0", "@mui/icons-material": "^7.3.5", "@mui/material": "^7.3.6", - "abcolor": "^0.5.5", "ace-builds": "^1.43.5", "air-datepicker": "^3.6.0", "dota2-emoticons": "^1.0.4", @@ -112,7 +111,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -2175,7 +2173,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2845,7 +2842,6 @@ "version": "7.3.6", "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.6.tgz", "integrity": "sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.6", @@ -3651,7 +3647,6 @@ "version": "17.0.47", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.47.tgz", "integrity": "sha512-mk0BL8zBinf2ozNr3qPnlu1oyVTYq+4V7WA76RgxUAtf0Em/Wbid38KN6n4abEkvO4xMTBWmnP1FtQzgkEiJoA==", - "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3755,14 +3750,6 @@ "integrity": "sha512-UMKd/doXE3karDUd5l+2LqFKUU4SmuPWOT/VA22OrPOUGMlb8/qQvSf+DBLQf/sd8UphuBGxOK/jB/1BB5oQRw==", "dev": true }, - "node_modules/abcolor": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/abcolor/-/abcolor-0.5.5.tgz", - "integrity": "sha512-xYDsPOWM2/n6Xvc9mwcJ1BIHdk6ahjWIqp5dwx9P+s0Ou/f6moythvJOdUNTdeRkB18e0yGin4DBZUksoaEQiw==", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/ace-builds": { "version": "1.43.5", "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.43.5.tgz", @@ -4109,7 +4096,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -6924,7 +6910,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -7028,7 +7013,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -7065,7 +7049,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -7387,7 +7370,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "peer": true, "dependencies": { "@babel/runtime": "^7.9.2" } @@ -7897,7 +7879,6 @@ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.4.1.tgz", "integrity": "sha512-RNqj14kYzw++6Sr38n7197xG33ipEOktGElty4I70IKzQF1jzaD1U4xQ+Ny/i03UUhHlC5NWEO+d8olRCDji6g==", "hasInstallScript": true, - "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.0.0", "@babel/traverse": "^7.0.0", @@ -7926,8 +7907,7 @@ "node_modules/stylis": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", - "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==", - "peer": true + "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==" }, "node_modules/stylis-rule-sheet": { "version": "0.0.10", @@ -8715,7 +8695,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -9324,7 +9303,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -9613,7 +9591,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", "dev": true, - "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -11035,7 +11012,6 @@ "version": "11.14.0", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", - "peer": true, "requires": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -11383,7 +11359,6 @@ "version": "7.3.6", "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.6.tgz", "integrity": "sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==", - "peer": true, "requires": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.6", @@ -11862,7 +11837,6 @@ "version": "17.0.47", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.47.tgz", "integrity": "sha512-mk0BL8zBinf2ozNr3qPnlu1oyVTYq+4V7WA76RgxUAtf0Em/Wbid38KN6n4abEkvO4xMTBWmnP1FtQzgkEiJoA==", - "peer": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -11963,11 +11937,6 @@ "integrity": "sha512-UMKd/doXE3karDUd5l+2LqFKUU4SmuPWOT/VA22OrPOUGMlb8/qQvSf+DBLQf/sd8UphuBGxOK/jB/1BB5oQRw==", "dev": true }, - "abcolor": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/abcolor/-/abcolor-0.5.5.tgz", - "integrity": "sha512-xYDsPOWM2/n6Xvc9mwcJ1BIHdk6ahjWIqp5dwx9P+s0Ou/f6moythvJOdUNTdeRkB18e0yGin4DBZUksoaEQiw==" - }, "ace-builds": { "version": "1.43.5", "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.43.5.tgz", @@ -12221,7 +12190,6 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", "dev": true, - "peer": true, "requires": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -14113,7 +14081,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "peer": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -14184,7 +14151,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "peer": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -14208,7 +14174,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "peer": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -14462,7 +14427,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "peer": true, "requires": { "@babel/runtime": "^7.9.2" } @@ -14834,7 +14798,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.4.1.tgz", "integrity": "sha512-RNqj14kYzw++6Sr38n7197xG33ipEOktGElty4I70IKzQF1jzaD1U4xQ+Ny/i03UUhHlC5NWEO+d8olRCDji6g==", - "peer": true, "requires": { "@babel/helper-module-imports": "^7.0.0", "@babel/traverse": "^7.0.0", @@ -14861,8 +14824,7 @@ "stylis": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", - "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==", - "peer": true + "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==" }, "stylis-rule-sheet": { "version": "0.0.10", @@ -15453,8 +15415,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "peer": true + "dev": true } } }, @@ -15804,8 +15765,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "peer": true + "dev": true } } }, diff --git a/src/components/Matches/Matches.tsx b/src/components/Matches/Matches.tsx index 107cb554ae..d7fbd7df95 100644 --- a/src/components/Matches/Matches.tsx +++ b/src/components/Matches/Matches.tsx @@ -7,7 +7,6 @@ import { transformations, subTextStyle, rankTierToString } from "../../utility"; import { getProMatches, getPublicMatches } from "../../actions"; import Table from "../Table/Table"; import TableLink from "../Table/TableLink"; -// import Heading from '../Heading'; import { IconTrophy } from "../Icons"; import Match from "../Match/Match"; import TabBar from "../TabBar/TabBar"; @@ -97,6 +96,67 @@ const matchesColumns = (strings: Strings) => [ }, ]; +// Columns used inside an expanded series. Same look as the pro tab but +// without the league name (it now lives on the parent series row). +const seriesMatchesColumns = (strings: Strings) => [ + { + displayName: strings.th_match_id, + field: "match_id", + sortFn: true, + displayFn: (row: any, col: any, field: any) => ( +
+ {field} +
+ +
+
+ ), + }, + { + displayName: strings.th_duration, + tooltip: strings.tooltip_duration, + field: "duration", + sortFn: true, + displayFn: transformations.duration, + }, + { + displayName: ( + + {strings.general_radiant} + + ), + field: "radiant_name", + color: constants.colorGreen, + displayFn: (row: any, col: any, field: any) => ( +
+ {row.radiant_win && ( + + + + )} + {field} +
+ ), + }, + { + displayName: ( + {strings.general_dire} + ), + field: "dire_name", + color: constants.colorRed, + displayFn: (row: any, col: any, field: any) => ( +
+ {!row.radiant_win && ( + + + + )} + {field} +
+ ), + }, +]; + const publicMatchesColumns = (strings: Strings) => [ { displayName: strings.th_match_id, @@ -160,6 +220,220 @@ const publicMatchesColumns = (strings: Strings) => [ }, ]; +// --------------------------------------------------------------------------- +// Series tab +// --------------------------------------------------------------------------- + +// A series is identified by series_id. Matches without a series_id are each +// treated as their own single-match "series" (matching the existing UI). +const groupBySeries = (matches: any[]) => { + const map = new Map(); + (matches || []).forEach((m) => { + const key = + m.series_id != null ? `s-${m.series_id}` : `m-${m.match_id}`; + if (!map.has(key)) map.set(key, []); + map.get(key)!.push(m); + }); + + // Insertion order follows the original (most-recent-first) array, so the + // series with the freshest game bubble to the top automatically. + return Array.from(map.entries()).map(([key, seriesMatches]) => ({ + key, + seriesId: seriesMatches[0].series_id, + matches: seriesMatches, // already sorted newest -> oldest + latestMatch: seriesMatches[0], + })); +}; + +const SERIES_GRID = "40px minmax(180px, 1.3fr) 1fr 1fr 90px 140px"; + +const SeriesWrapper = styled.div` + width: 100%; + background: #1b2330; + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 3px; + overflow: hidden; +`; + +const SeriesGrid = styled.div` + display: grid; + grid-template-columns: ${SERIES_GRID}; + align-items: center; + column-gap: 12px; + padding: 12px 14px; +`; + +const SeriesHeaderGrid = styled(SeriesGrid)` + background: #1e2633; + color: #8fa3bd; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +`; + +const SeriesRowGrid = styled(SeriesGrid)` + background: #1b2330; + color: #dfe6ee; + font-size: 14px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + cursor: pointer; + transition: background 0.15s ease; + + &:hover { + background: #222c3d; + } +`; + +const Toggle = styled.span` + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 3px; + background: rgba(255, 255, 255, 0.08); + color: #9fb0c7; + font-size: 13px; + line-height: 1; + user-select: none; +`; + +const TeamOne = styled.span` + color: ${constants.colorGreen}; +`; + +const TeamTwo = styled.span` + color: ${constants.colorRed}; +`; + +// NOTE: `subTextStyle` is a plain CSSProperties object, so it can't be +// interpolated directly into a styled-components template literal. We apply +// it inline on the element instead. +const SeriesSubText = styled.div` + margin-top: 3px; +`; + +const SeriesMatchWrap = styled.div` + background: #161d28; + + /* Kill the leftover spacing the Table component carries at its tail */ + & > *:last-child { + margin-bottom: 0 !important; + padding-bottom: 0 !important; + border-bottom: none !important; + } + + /* Remove the border on the very last match row so the expanded body + ends cleanly on the row itself */ + table { + margin-bottom: 0; + } + + tbody tr:last-child td, + tbody tr:last-child th { + border-bottom: none !important; + } +`; + +const SeriesTab = ({ + data, + loading, + strings, +}: { + data: any[]; + loading: boolean; + strings: Strings; +}) => { + const [expandedKey, setExpandedKey] = React.useState(null); + const seriesList = React.useMemo(() => groupBySeries(data), [data]); + + if (loading) { + return
Loading series...
; + } + + return ( + + +
+
{strings.th_series}
+
{strings.th_team_1}
+
{strings.th_team_2}
+
{strings.th_matches}
+
{strings.th_last_played}
+ + + {seriesList.map((series) => { + const isExpanded = expandedKey === series.key; + const { latestMatch } = series; + + return ( +
+ setExpandedKey(isExpanded ? null : series.key)} + > +
+ {isExpanded ? "−" : "+"} +
+ +
+ {series.seriesId != null ? ( + // `TableLink` doesn't forward `onClick`, so we wrap it in a + // plain div that stops the click from toggling the row. +
e.stopPropagation()}> + + {series.seriesId} + +
+ ) : ( + — + )} + + {(latestMatch.league_name || "").trim()} + +
+ +
+ {latestMatch.radiant_name} +
+ +
+ {latestMatch.dire_name} +
+ +
+ {series.matches.length}{" "} + {series.matches.length === 1 ? "match" : "matches"} +
+ +
+ +
+
+ + {isExpanded && ( + + + + )} + + ); + })} + + ); +}; + +// --------------------------------------------------------------------------- +// Tabs +// --------------------------------------------------------------------------- + const matchTabs = (strings: Strings) => [ { name: strings.hero_pro_tab, @@ -176,6 +450,18 @@ const matchTabs = (strings: Strings) => [ ), route: "/matches/pro", }, + { + name: strings.matches_series, + key: "series", + content: (propsPar: MatchesProps) => ( + + ), + route: "/matches/series", + }, { name: strings.matches_highest_mmr, key: "highMmr", @@ -221,6 +507,7 @@ class Matches extends React.Component { getData(this.props); } } + render() { const { strings } = this.props; const route = this.props.match.params.matchId || "pro"; @@ -253,4 +540,4 @@ const mapDispatchToProps = (dispatch: any) => ({ dispatchPublicMatches: (options: any) => dispatch(getPublicMatches(options)), }); -export default connect(mapStateToProps, mapDispatchToProps)(Matches); +export default connect(mapStateToProps, mapDispatchToProps)(Matches); \ No newline at end of file diff --git a/src/lang/en-US.json b/src/lang/en-US.json index 12689dec05..5d27b815f3 100644 --- a/src/lang/en-US.json +++ b/src/lang/en-US.json @@ -540,6 +540,7 @@ "match_ban": "Ban", "matches_highest_mmr": "Top Public", "matches_lowest_mmr": "Low MMR", + "matches_series": "Series", "meta_title": "Meta", "meta_description": "Run advanced queries on data from recent sampled public matches", "mmr_not_up_to_date": "Why is the MMR not up to date?", @@ -780,6 +781,9 @@ "th_category": "Category", "th_matches": "Matches", "th_percentile": "Percentile", + "th_series": "Series", + "th_team_1": "Team 1", + "th_team_2": "Team 2", "th_rank": "Rank", "th_items": "Items", "th_stacked": "Stacked",