diff --git a/components/match_ticker/commons/match_ticker.lua b/components/match_ticker/commons/match_ticker.lua
index 89040bc6af2..04e3617728a 100644
--- a/components/match_ticker/commons/match_ticker.lua
+++ b/components/match_ticker/commons/match_ticker.lua
@@ -58,6 +58,17 @@ local DEFAULT_RECENT_ORDER = 'date desc, liquipediatier asc, tournament asc'
local DEFAULT_LIVE_HOURS = 8
local NOW = os.date('%Y-%m-%d %H:%M', os.time(os.date('!*t') --[[@as osdateparam]]))
+--- Extract externally if it grows
+---@param matchTickerConfig MatchTickerConfig
+---@return unknown # Todo: Add interface for MatchTickerDisplay
+local MatchTickerDisplayFactory = function (matchTickerConfig)
+ if matchTickerConfig.newStyle then
+ return Lua.import('Module:MatchTicker/DisplayComponents/New')
+ else
+ return Lua.import('Module:MatchTicker/DisplayComponents')
+ end
+end
+
---@class MatchTickerConfig
---@field tournaments string[]
---@field limit integer
@@ -80,6 +91,7 @@ local NOW = os.date('%Y-%m-%d %H:%M', os.time(os.date('!*t') --[[@as osdateparam
---@field onlyHighlightOnValue string?
---@field tiers string[]?
---@field tierTypes string[]?
+---@field newStyle boolean?
---@class MatchTicker
---@operator call(table): MatchTicker
@@ -88,8 +100,6 @@ local NOW = os.date('%Y-%m-%d %H:%M', os.time(os.date('!*t') --[[@as osdateparam
---@field matches table[]?
local MatchTicker = Class.new(function(self, args) self:init(args) end)
-MatchTicker.DisplayComponents = Lua.import('Module:MatchTicker/DisplayComponents')
-
---@param args table?
---@return table
function MatchTicker:init(args)
@@ -124,6 +134,7 @@ function MatchTicker:init(args)
tierTypes = args.tiertypes and Array.filter(
Array.parseCommaSeparatedString(args.tiertypes), FnUtil.curry(Tier.isValid, 1)
) or nil,
+ newStyle = Logic.readBool(args.newStyle),
}
--min 1 of them has to be set; recent can not be set while any of the others is set
@@ -164,6 +175,8 @@ function MatchTicker:init(args)
end
config.wrapperClasses = wrapperClasses
+ MatchTicker.DisplayComponents = MatchTickerDisplayFactory(config)
+
self.config = config
return self
diff --git a/components/match_ticker/commons/match_ticker_custom.lua b/components/match_ticker/commons/match_ticker_custom.lua
index 609882795e7..7ce33296b20 100644
--- a/components/match_ticker/commons/match_ticker_custom.lua
+++ b/components/match_ticker/commons/match_ticker_custom.lua
@@ -45,6 +45,15 @@ function CustomMatchTicker.mainPage(frame)
return MatchTicker(args):query():create()
end
+---Entry point for display on the main page with the new style
+---@param frame Frame?
+---@return Html
+function CustomMatchTicker.newMainPage(frame)
+ local args = Arguments.getArgs(frame)
+ args.newStyle = true
+ return MatchTicker(args):query():create():addClass('new-match-style')
+end
+
---Entry point for display on player pages
---@param frame Frame?
---@return Html
diff --git a/components/match_ticker/commons/match_ticker_display_components.lua b/components/match_ticker/commons/match_ticker_display_components.lua
index 5c867587eb6..6e54c949db8 100644
--- a/components/match_ticker/commons/match_ticker_display_components.lua
+++ b/components/match_ticker/commons/match_ticker_display_components.lua
@@ -98,7 +98,7 @@ function Versus:create()
return self.root
:node(mw.html.create('div')
- :css('line-height', '1.1'):node(upperText or VS)
+ :addClass('versus-upper'):node(upperText or VS)
):node(mw.html.create('div')
:addClass('versus-lower'):wikitext('(' .. lowerText .. ')')
)
@@ -123,6 +123,7 @@ function Versus:scores()
local scores, scores2 = {}, {}
local hasSecondScore
+ local delimiter = ':'
local setWinner = function(score, opponentIndex)
if winner == opponentIndex then
@@ -135,20 +136,20 @@ function Versus:scores()
local score = Logic.isNotEmpty(opponent.status) and opponent.status ~= SCORE_STATUS and opponent.status
or tonumber(opponent.score) or -1
- table.insert(scores, setWinner(score ~= -1 and score or 0, opponentIndex))
+ table.insert(scores, '' .. setWinner(score ~= -1 and score or 0, opponentIndex) .. '' )
local score2 = tonumber((opponent.extradata or {}).score2) or 0
- table.insert(scores2, setWinner(score2, opponentIndex))
+ table.insert(scores2, '' .. setWinner(score2, opponentIndex) .. '' )
if score2 > 0 then
hasSecondScore = true
end
end)
if hasSecondScore then
- return table.concat(scores, ':'), table.concat(scores2, ':')
+ return table.concat(scores, delimiter), table.concat(scores2, delimiter)
end
- return table.concat(scores, ':')
+ return table.concat(scores, delimiter)
end
---Display class for matches shown within a match ticker
diff --git a/components/match_ticker/commons/match_ticker_display_components_new.lua b/components/match_ticker/commons/match_ticker_display_components_new.lua
new file mode 100644
index 00000000000..9473a9ff0db
--- /dev/null
+++ b/components/match_ticker/commons/match_ticker_display_components_new.lua
@@ -0,0 +1,240 @@
+---
+-- @Liquipedia
+-- wiki=commons
+-- page=Module:MatchTicker/DisplayComponents/New
+--
+-- Please see https://github.com/Liquipedia/Lua-Modules to contribute
+--
+
+-- Holds DisplayComponents for the MatchTicker module
+-- It contains the new html structure intented to be use for the new Dota2 Main Page (for now)
+-- Will most likely be expanded to other games in the future and other pages
+
+local Class = require('Module:Class')
+local Countdown = require('Module:Countdown')
+local DateExt = require('Module:Date/Ext')
+local LeagueIcon = require('Module:LeagueIcon')
+local Logic = require('Module:Logic')
+local Lua = require('Module:Lua')
+local Timezone = require('Module:Timezone')
+local StreamLinks = require('Module:Links/Stream')
+local Page = require('Module:Page')
+local DefaultMatchTickerDisplayComponents = require('Module:MatchTicker/DisplayComponents')
+
+local HighlightConditions = Lua.import('Module:HighlightConditions')
+
+local OpponentLibraries = require('Module:OpponentLibraries')
+local Opponent = OpponentLibraries.Opponent
+local OpponentDisplay = OpponentLibraries.OpponentDisplay
+
+local CURRENT_PAGE = mw.title.getCurrentTitle().text
+local HIGHLIGHT_CLASS = 'tournament-highlighted-bg'
+local TOURNAMENT_DEFAULT_ICON = 'Generic_Tournament_icon.png'
+
+---Display class for matches shown within a match ticker
+---@class NewMatchTickerScoreBoard
+---@operator call(table): NewMatchTickerScoreBoard
+---@field root Html
+---@field match table
+local ScoreBoard = Class.new(
+ function(self, match)
+ self.root = mw.html.create('div'):addClass('match-scoreboard')
+ self.match = match
+ end
+)
+
+---@return Html
+function ScoreBoard:create()
+ local match = self.match
+ local winner = tonumber(match.winner)
+
+ return self.root
+ :node(self:opponent(match.match2opponents[1], winner == 1, true):addClass('team-left'))
+ :node(self:versus())
+ :node(self:opponent(match.match2opponents[2], winner == 2):addClass('team-right'))
+end
+
+---@param opponentData table
+---@param isWinner boolean
+---@param flip boolean?
+---@return Html
+function ScoreBoard:opponent(opponentData, isWinner, flip)
+ local opponent = Opponent.fromMatch2Record(opponentData)
+ ---@cast opponent -nil
+ if Opponent.isEmpty(opponent) or Opponent.isTbd(opponent) and opponent.type ~= Opponent.literal then
+ opponent = Opponent.tbd(Opponent.literal)
+ end
+
+ local opponentName = Opponent.toName(opponent)
+ if not opponentName then
+ mw.logObject(opponent, 'Invalid Opponent, Opponent.toName returns nil')
+ opponentName = ''
+ end
+
+ local opponentDisplay = mw.html.create('div')
+ :node(OpponentDisplay.InlineOpponent{
+ opponent = opponent,
+ teamStyle = 'short',
+ flip = flip,
+ showLink = opponentName:gsub('_', ' ') ~= CURRENT_PAGE
+ })
+
+ if isWinner then
+ opponentDisplay:addClass('match-winner')
+ end
+
+ return opponentDisplay
+end
+
+---@return Html
+function ScoreBoard:versus()
+ return mw.html.create('div')
+ :addClass('versus')
+ :node(DefaultMatchTickerDisplayComponents.Versus(self.match):create())
+end
+
+---Display class for the details of a match displayed at the bottom of a match ticker
+---@class NewMatchTickerDetails
+---@operator call(table): NewMatchTickerMatch
+---@field root Html
+---@field hideTournament boolean
+---@field onlyHighlightOnValue string?
+---@field match table
+local Details = Class.new(
+ function(self, args)
+ assert(args.match, 'No Match passed to MatchTickerDetails class')
+ self.root = mw.html.create('div'):addClass('match-details')
+ self.hideTournament = args.hideTournament
+ self.onlyHighlightOnValue = args.onlyHighlightOnValue
+ self.match = args.match
+ end
+)
+
+---@return Html
+function Details:create()
+ local highlightCondition = HighlightConditions.match or HighlightConditions.tournament
+ if highlightCondition(self.match, {onlyHighlightOnValue = self.onlyHighlightOnValue}) then
+ self.root:addClass(HIGHLIGHT_CLASS)
+ end
+
+ return self.root
+ :node(self:streams())
+ :node(self:tournament())
+ :node(self:countdown())
+end
+
+---It will display both countdown and date of the match so the user can select which one to show
+---@return Html
+function Details:countdown()
+ local match = self.match
+
+ local dateString
+ if Logic.readBool(match.dateexact) then
+ local timestamp = DateExt.readTimestamp(match.date) + (Timezone.getOffset(match.extradata.timezoneid) or 0)
+ dateString = DateExt.formatTimestamp('F j, Y - H:i', timestamp) .. ' '
+ .. (Timezone.getTimezoneString(match.extradata.timezoneid) or (Timezone.getTimezoneString('UTC')))
+ else
+ dateString = mw.getContentLanguage():formatDate('F j, Y', match.date) .. (Timezone.getTimezoneString('UTC'))
+ end
+
+ local countdownArgs = {
+ date = dateString,
+ finished = match.finished
+ }
+
+ local countdownDisplay = mw.html.create('span')
+ :addClass('match-countdown')
+ :node(Countdown._create(countdownArgs))
+
+ return countdownDisplay
+end
+
+---@return Html?
+function Details:streams()
+ local match = self.match
+ local links = mw.html.create('div')
+ :addClass('match-streams')
+
+ links:wikitext(table.concat(StreamLinks.buildDisplays(StreamLinks.filterStreams(match.stream)) or {}))
+
+ return links
+end
+
+---@return Html?
+function Details:tournament()
+ if self.hideTournament then
+ return
+ end
+
+ local match = self.match
+
+ local icon = LeagueIcon.display{
+ icon = Logic.emptyOr(match.icon, TOURNAMENT_DEFAULT_ICON),
+ iconDark = match.icondark,
+ link = match.pagename,
+ name = match.tournament,
+ options = {noTemplate = true},
+ }
+
+ local displayName = Logic.emptyOr(
+ match.tickername,
+ match.tournament,
+ match.parent:gsub('_', ' ')
+ )
+
+ return mw.html.create('div')
+ :addClass('match-tournament')
+ :node(mw.html.create('div')
+ :addClass('tournament-icon')
+ :node(mw.html.create('div')
+ :wikitext(icon)
+ )
+ )
+ :node(mw.html.create('div')
+ :addClass('tournament-text')
+ :wikitext(Page.makeInternalLink({}, displayName, match.pagename))
+ )
+
+end
+
+---Display class for matches shown within a match ticker
+---@class NewMatchTickerMatch
+---@operator call({config: MatchTickerConfig, match: table}): NewMatchTickerMatch
+---@field root Html
+---@field config MatchTickerConfig
+---@field match table
+local Match = Class.new(
+ function(self, args)
+ self.root = mw.html.create('div'):addClass('match')
+ self.config = args.config
+ self.match = args.match
+ end
+)
+
+---@return Html
+function Match:create()
+ self.root:node(self:standardMatchRow())
+ self.root:node(self:detailsRow())
+
+ return self.root
+end
+
+---@return Html
+function Match:standardMatchRow()
+ return ScoreBoard(self.match):create()
+end
+
+---@return Html
+function Match:detailsRow()
+ return Details{
+ match = self.match,
+ hideTournament = self.config.hideTournament,
+ onlyHighlightOnValue = self.config.onlyHighlightOnValue
+ }:create()
+end
+
+return {
+ Match = Match,
+ Details = Details,
+ ScoreBoard = ScoreBoard,
+}
diff --git a/stylesheets/commons/MatchTicker.less b/stylesheets/commons/MatchTicker.less
new file mode 100644
index 00000000000..78a4381c707
--- /dev/null
+++ b/stylesheets/commons/MatchTicker.less
@@ -0,0 +1,144 @@
+/*******************************************************************************
+Template(s): Match Ticker
+Author(s): Nadox
+*******************************************************************************/
+
+// Backwards compatibility for previous display component
+.versus-upper {
+ line-height: 1.1;
+}
+
+.new-match-style {
+ .match-section-header {
+ display: flex;
+ flex-direction: column;
+ padding: 1rem;
+ gap: 0.5em;
+ }
+
+ .match {
+ border-bottom: 1px solid var( --table-border-color, var( --clr-border, #bbbbbb ) );
+ padding: 0.75rem 1rem;
+ font-size: 0.875rem;
+
+ .match-scoreboard {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 2rem;
+ margin-bottom: 0.5rem;
+
+ > div {
+ flex: 1;
+ }
+
+ .team-left {
+ display: flex;
+ justify-content: right;
+ }
+
+ .versus {
+ flex: 0 1 auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+
+ .versus-upper {
+ display: flex;
+ gap: 0.25rem;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .versus-lower {
+ font-size: 0.75rem;
+ color: var( --clr-secondary );
+ text-align: center;
+ }
+ }
+
+ .match-winner {
+ font-weight: bold;
+ }
+ }
+
+ .match-details {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.5rem 0.75rem;
+ border-radius: 0.5rem;
+ font-size: 0.75rem;
+
+ .theme--light & {
+ background-color: #0000000a;
+ }
+
+ .theme--dark & {
+ background-color: #ffffff0a;
+ }
+
+ .match-streams {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.25rem;
+ }
+
+ .match-tournament {
+ display: flex;
+ gap: 0.5rem;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .timer-object-countdown,
+ .timer-object-date {
+ padding: 0.25rem 0.5rem;
+ border-radius: 0.25rem;
+ font-weight: bold;
+
+ &.timer-object-countdown-live {
+ .theme--light & {
+ color: #ffffff;
+ background-color: #b81414;
+ }
+
+ .theme--dark & {
+ color: #2e0505;
+ background-color: #f5a3a3;
+ }
+ }
+
+ &.timer-object-countdown-completed {
+ .theme--light & {
+ color: #ffffff;
+ background-color: #1d7c1d;
+ }
+
+ .theme--dark & {
+ color: #0a290a;
+ background-color: #adebad;
+ }
+ }
+
+ &:not( .timer-object-countdown-live, .timer-object-countdown-completed ) {
+ color: var( --clr-on-background );
+
+ .theme--light & {
+ background-color: #00000014;
+ }
+
+ .theme--dark & {
+ background-color: #ffffff14;
+ }
+ }
+ }
+ }
+
+ .timer-object-separator {
+ display: none;
+ }
+ }
+}