Module:ArchiveTeam portal list: Difference between revisions

From Liquipedia Marvel Rivals Wiki
No edit summary
Tag: Manual revert
test
Line 1: Line 1:
local Lua = require('Module:Lua')
local Lua = require('Module:Lua')
local Arguments = require('Module:Arguments')
local Lpdb = require('Module:Lpdb')


local Arguments = require('Module:Arguments')
local Countdown = Lua.import('Module:Countdown')
local Countdown = Lua.import('Module:Countdown')
local DateExt = Lua.import('Module:Date/Ext')
local DateExt = Lua.import('Module:Date/Ext')
local Flags = Lua.import('Module:Flags')
local Flags = Lua.import('Module:Flags')


local HtmlWidgets = Lua.import('Module:Widget/Html/All')
local Widgets = Lua.import('Module:Widget/Html/All')
local Div = HtmlWidgets.Div
local Div = Widgets.Div
local Span = HtmlWidgets.Span
local Span = Widgets.Span
local Link = Lua.import('Module:Widget/Basic/Link')
local Button = Lua.import('Module:Widget/Basic/Button')
local Image = Lua.import('Module:Widget/Image')


local MAX_PLAYERS = 8
local TeamPortal = {}
local DEFAULT_TEAMS = 10


local TeamPortal = {}
local config = {
MAX_PLAYERS = 8,
DEFAULT_TEAMS = 10,
BLANK_PLAYER_IMAGE = 'Blank Player Image nopadding.png'
}


-- Sanitizes a class name for use in HTML attributes
---Sanitizes a string for use as an HTML class name.
-- Replaces spaces with underscores, encodes special characters, and ensures valid CSS class naming
---@param class string
---@param class string
---@return string
---@return string
Line 31: Line 37:
end
end


-- Creates a team portal displaying team rankings, rosters, next matches, and latest transfers
---Creates and renders the team portal.
-- Fetches data from LiquipediaDB and constructs HTML output
---@param frame table
-- @param frame table MediaWiki frame object containing invocation arguments
---@return string
-- @return string HTML string of the team portal
function TeamPortal.create(frame)
function TeamPortal.create(frame)
local args = Arguments.getArgs(frame)
local args = Arguments.getArgs(frame)
local lang = mw.language.getContentLanguage()
local lang = mw.language.getContentLanguage()


-- Fetch team data from LiquipediaDB, sorted by earnings
local lpdbData = Lpdb.call('team', {
local lpdbData = mw.ext.LiquipediaDB.lpdb('team', {
limit = args.teams or config.DEFAULT_TEAMS,
limit = DEFAULT_TEAMS,
offset = 0,
conditions = '[[pageid::!0]] AND [[disbanddate::' .. DateExt.defaultDate .. ']]',
conditions = '[[pageid::!0]] AND [[disbanddate::' .. DateExt.defaultDate .. ']]',
order = 'earnings desc',
order = 'earnings desc',
Line 48: Line 51:
})
})


-- Create the main wrapper div
local wrapper = Div{}
local portalItems = {}
local portalItems = {}
 
for i, teamData in ipairs(lpdbData) do
local counter = 1
 
for _, teamData in ipairs(lpdbData) do
local teamName = teamData.name
local teamName = teamData.name
local teamNameUc = teamName:gsub('^%l', mw.ustring.upper)
local teamPageName = teamData.pagename
local teamPageName = teamData.pagename
local teamEarnings = lang:formatNum(tonumber(teamData.earnings))
local teamEarnings = lang:formatNum(tonumber(teamData.earnings))
local teamId = mw.ustring.lower(teamPageName:gsub('_', '-'))
local sanitizedTeamId = TeamPortal._sanitizeClass(teamPageName)
local sanitizedTeamId = TeamPortal._sanitizeClass(teamId)


-- Create card title with team rank, icon, name, and earnings
-- Create card title
local titleSpans = {
Span{classes = {'tp-rank-number'}, children = {'#' .. counter}},
mw.ext.TeamTemplate.teamicon(teamName),
Span{classes = {'tp-team-name'}, children = {teamName}},
Span{classes = {'tp-total-earnings'}, children = {
Span{classes = {'tp-total-earnings-text'}, children = {'Total Earnings:'}},
' $' .. teamEarnings
}}
}
 
-- Create team card title
local cardTitle = Div{
local cardTitle = Div{
classes = {'tp-rank-card-title', 'mw-customtoggle-' .. sanitizedTeamId},
classes = {'tp-rank-card-title', 'mw-customtoggle-' .. sanitizedTeamId},
children = titleSpans
children = {
Span{classes = {'tp-rank-number'}, children = {'#' .. i}},
mw.ext.TeamTemplate.teamicon(teamName),
Span{classes = {'tp-team-name'}, children = {teamName}},
Span{classes = {'tp-total-earnings'}, children = {
Span{classes = {'tp-total-earnings-text'}, children = {'Total Earnings:'}},
' $' .. teamEarnings
}}
}
}
}


-- Squad player section
-- Build collapsible content sections
local playerRows = TeamPortal._buildPlayerRows(teamPageName, maxPlayers)
local playerContainer = TeamPortal._buildPlayerSection(teamPageName)
 
-- Create player rows container
local playerContainer = Div{
css = {
['display'] = 'flex',
['flex-wrap'] = 'wrap',
['justify-content'] = 'center',
['align-items'] = 'flex-start',
['gap'] = '15px',
['padding'] = '5px 0'
},
children = playerRows
}
 
-- Next match section
local nextMatchSection = TeamPortal._buildNextMatchSection(teamName)
local nextMatchSection = TeamPortal._buildNextMatchSection(teamName)
 
local latestTransferSection = TeamPortal._buildLatestTransferSection(teamName, lang)
-- Latest transfer section
local buttonRow = TeamPortal._buildButtonRow(teamPageName)
local latestTransferSection = TeamPortal._buildLatestTransferSection(teamNameUc, lang)
 
-- Button row
local buttonLinks = {
'[[' .. teamPageName .. '|<div class="tp-button">Team Page</div>]]',
'[[' .. teamPageName .. '/Results|<div class="tp-button">Results</div>]]',
'[[' .. teamPageName .. '/Played Matches|<div class="tp-button">Played Matches</div>]]'
}
local buttonRow = Div{
classes = {'tp-button-row'},
children = {table.concat(buttonLinks)}
}


-- Assemble the team box
-- Assemble the team box
Line 119: Line 85:
}
}
-- Add collapsed class if not the first team
if i ~= 1 then
if counter ~= 1 then
table.insert(teamBox.props.classes, 'mw-collapsed')
table.insert(teamBox.props.classes, 'mw-collapsed')
end
end


-- Create final card structure
local card = Div{classes = {'tp-rank-card'}, children = {cardTitle, teamBox}}
local card = Div{
table.insert(portalItems, Div{classes = {'team-portal-list'}, children = {card}})
classes = {'tp-rank-card'},
children = {cardTitle, teamBox}
}
local cardWrapper = Div{
classes = {'team-portal-list'},
children = {card}
}
 
table.insert(portalItems, cardWrapper)
counter = counter + 1
end
end


-- Add all items to the wrapper
return tostring(Div{children = portalItems}:render())
wrapper.props.children = portalItems
 
-- Convert to HTML string
return tostring(wrapper:render())
end
end


-- Builds player rows for a team
---Builds the player display section.
-- @param teamPageName string Name of the team page
---@param teamPageName string
-- @param maxPlayers number Maximum number of players to display
---@return WidgetDiv
-- @return table Array of player row HTML elements
function TeamPortal._buildPlayerSection(teamPageName)
function TeamPortal._buildPlayerRows(teamPageName, maxPlayers)
local squadData = Lpdb.call('squadplayer', {
local squadData = mw.ext.LiquipediaDB.lpdb('squadplayer', {
limit = config.MAX_PLAYERS,
limit = MAX_PLAYERS,
offset = 0,
conditions = '[[pagename::' .. teamPageName .. ']] AND [[leavedate::' .. DateExt.defaultDate .. ']] '
conditions = '[[pagename::' .. teamPageName .. ']] AND [[leavedate::' .. DateExt.defaultDate .. ']] '
.. 'AND [[inactivedate::' .. DateExt.defaultDate .. ']] AND [[type::player]]',
.. 'AND [[inactivedate::' .. DateExt.defaultDate .. ']] AND [[type::player]]',
Line 161: Line 109:


local playerRows = {}
local playerRows = {}
local counterPlayer = 1
for i, squadPlayerData in ipairs(squadData) do
local playerData = Lpdb.call('player', {
for _, squadPlayerData in ipairs(squadData) do
conditions = '[[pageid::!0]] AND [[pagename::' .. (squadPlayerData.link or ''):gsub(' ', '_') .. ']]',
if counterPlayer > MAX_PLAYERS then break end
 
-- Fetch player data
local playerDataResult = mw.ext.LiquipediaDB.lpdb('player', {
conditions = '[[pageid::!0]] AND ([[pagename::' .. squadPlayerData.link .. ']] OR [[pagename::' .. squadPlayerData.link:gsub(' ', '_') .. ']])',
query = 'pagename, id, nationality, image',
query = 'pagename, id, nationality, image',
limit = 1
limit = 1
})
})[1] or {}
local playerPage = playerDataResult[1] and playerDataResult[1].pagename or ''
local displayId = playerDataResult[1] and playerDataResult[1].id or squadPlayerData.id or ''
local playerFlag = playerDataResult[1] and playerDataResult[1].nationality or ''
local playerImage = playerDataResult[1] and playerDataResult[1].image or ''
-- If no flag was found, try to get it from other sources
local playerPage = playerData.pagename or ''
playerFlag = TeamPortal._findPlayerFlag(playerFlag, squadPlayerData, displayId)
local displayId = playerData.id or squadPlayerData.id or ''
local playerFlag = playerData.nationality or ''


-- Create player image section
local playerImage = Image{
local imageLink = playerPage ~= '' and ('|link=' .. playerPage) or ''
page = playerData.image or config.BLANK_PLAYER_IMAGE,
local imageWikitext = playerImage ~= ''
width = 186, height = 140,
and '[[File:' .. playerImage .. '|186x140px' .. imageLink .. ']]'
link = playerPage ~= '' and playerPage or nil,
or '[[File:Blank Player Image nopadding.png|186x140px' .. imageLink .. ']]'
local playerImageDiv = Div{
classes = {'tp-photo'},
css = {['margin-bottom'] = '5px'},
children = {imageWikitext}
}
}
-- Create player name and flag section
local namePart = displayId ~= '' and (playerPage ~= '' and '[[' .. playerPage .. '|' .. displayId .. ']]' or displayId) or ''
local flagWikitext = playerFlag ~= '' and Flags.Icon({ flag = playerFlag, shouldLink = playerPage ~= '' }) or ''
local fullWikitext = flagWikitext .. (flagWikitext ~= '' and namePart ~= '' and '\t' or '') .. namePart
local playerName = Div{
local nameWidget = (displayId ~= '' and playerPage ~= '') and Link{page = playerPage, children = {displayId}} or displayId
classes = {'tp-name'},
local flagIcon = playerFlag ~= '' and Flags.Icon({ flag = playerFlag, shouldLink = playerPage ~= '' }) or ''
children = {fullWikitext}
}


-- Assemble player card
table.insert(playerRows, Div{
local playerCard = Div{
classes = {'tp-player-' .. i},
classes = {'tp-player-' .. counterPlayer},
css = {['text-align'] = 'center', ['margin-bottom'] = '2px'},
css = {['text-align'] = 'center', ['margin-bottom'] = '2px'},
children = {playerImageDiv, playerName}
children = {
}
Div{classes = {'tp-photo'}, css = {['margin-bottom'] = '5px'}, children = {playerImage}},
Div{classes = {'tp-name'}, children = {flagIcon, (flagIcon ~= '' and nameWidget ~= '') and '\t' or '', nameWidget}}
table.insert(playerRows, playerCard)
}
counterPlayer = counterPlayer + 1
end
return playerRows
end
 
-- Attempts to find a player flag from various data sources
-- @param currentFlag string Current flag value (may be empty)
-- @param squadPlayerData table Player data from squad table
-- @param playerId string Player ID to search for
-- @return string Player flag code or empty string if not found
function TeamPortal._findPlayerFlag(currentFlag, squadPlayerData, playerId)
if currentFlag ~= '' then return currentFlag end
-- Try squadplayer table first
if squadPlayerData.link ~= '' then
local squadPlayerNationalityData = mw.ext.LiquipediaDB.lpdb('squadplayer', {
conditions = '[[link::' .. squadPlayerData.link .. ']] AND [[nationality::!]]',
query = 'nationality',
limit = 1
})
if squadPlayerNationalityData[1] and squadPlayerNationalityData[1].nationality then
return squadPlayerNationalityData[1].nationality
end
end
-- If we have player ID, try placement table
if playerId ~= '' then
-- Search by participant link
local placementData = mw.ext.LiquipediaDB.lpdb('placement', {
conditions = '[[participantlink::' .. playerId .. ']] AND [[participantflag::!]]',
query = 'participantflag',
limit = 1
})
})
if placementData[1] and placementData[1].participantflag then
return placementData[1].participantflag
else
-- Try to find in players data within placement records
local playerPlacementData = mw.ext.LiquipediaDB.lpdb('placement', {
conditions = '[[players::' .. playerId .. ']] AND [[players::!]]',
query = 'players',
limit = 10
})
-- Check if player flag exists in any player slot
for _, record in ipairs(playerPlacementData) do
if record.players then
-- Check various player positions for flag info
for i = 1, 5 do
if record.players['p' .. i] == playerId and record.players['p' .. i .. 'flag'] and record.players['p' .. i .. 'flag'] ~= '' then
return record.players['p' .. i .. 'flag']
end
end
end
end
end
-- If still no flag, as a last resort, try to check if there's any record with the player ID as the exact name
local exactNameMatch = mw.ext.LiquipediaDB.lpdb('player', {
conditions = '[[id::' .. playerId .. ']] AND [[nationality::!]]',
query = 'nationality',
limit = 1
})
if exactNameMatch[1] and exactNameMatch[1].nationality then
return exactNameMatch[1].nationality
end
end
end
return ''
return Div{
css = {
display = 'flex', ['flex-wrap'] = 'wrap', ['justify-content'] = 'center',
['align-items'] = 'flex-start', gap = '15px', padding = '5px 0'
},
children = playerRows
}
end
end


-- Builds the next match section for a team
---Builds the next match section.
-- @param teamName string Name of the team
---@param teamName string
-- @return table HTML container for next match section
---@return WidgetDiv
function TeamPortal._buildNextMatchSection(teamName)
function TeamPortal._buildNextMatchSection(teamName)
-- Fetch next match data
local matchData = Lpdb.call('match', {
local matchData = mw.ext.LiquipediaDB.lpdb('match', {
limit = 1,
limit = 1,
offset = 0,
conditions = '[[pageid::!-1]] AND [[finished::0]] AND [[dateexact::1]] '
conditions = '[[pageid::!-1]] AND [[finished::0]] AND [[dateexact::1]] '
.. 'AND ([[opponent1::' .. teamName .. ']] OR [[opponent2::' .. teamName .. ']])',
.. 'AND ([[opponent1::' .. teamName .. ']] OR [[opponent2::' .. teamName .. ']])',
order = 'date asc',
order = 'date asc',
query = 'pagename, tickername, icon, opponent1, opponent2, date',
query = 'pagename, tickername, opponent1, opponent2, date',
})
})[1]


local tournamentPage = matchData[1] and matchData[1].pagename or ''
local elements = {'Next Match'}
local tournamentName = matchData[1] and matchData[1].tickername or ''
if matchData then
local tournamentIcon = matchData[1] and matchData[1].icon or ''
local opponent = (teamName == matchData.opponent1) and matchData.opponent2 or matchData.opponent1
local team1 = matchData[1] and matchData[1].opponent1 or ''
table.insert(elements, Span{classes = {'tp-vs-team'}, children = {'<br>' .. (opponent ~= 'TBD' and mw.ext.TeamTemplate.team(opponent) or 'TBD')}})
local team2 = matchData[1] and matchData[1].opponent2 or ''
local matchDate = matchData[1] and matchData[1].date or ''
local tourneyLink = Link{page = matchData.pagename, children = {matchData.tickername or matchData.pagename}}
local cdHtml = matchDate ~= '' and Countdown.create({ date = matchDate }) or ''
table.insert(elements, Span{classes = {'tp-tournament'}, children = {'<br>', tourneyLink}})
 
-- Next match elements
if matchData.date then
local nextMatchElements = {
table.insert(elements, Span{classes = {'tp-countdown'}, children = {'<br>', Countdown.create({ date = matchData.date })}})
'Next Match'
}
if matchData[1] then
local opponentDisplay = teamName == team1 and team2 or team1
if opponentDisplay ~= '' then
table.insert(nextMatchElements,  
Span{
classes = {'tp-vs-team'},
children = {'<br>' .. (opponentDisplay ~= 'TBD' and mw.ext.TeamTemplate.team(opponentDisplay) or 'TBD')}
}
)
else
table.insert(nextMatchElements,  
Span{classes = {'tp-vs-team'}, children = {'<br>vs ?'}}
)
end
end
if tournamentPage ~= '' then
local tournamentLinkText = tournamentName ~= '' and tournamentName or tournamentPage
local tournamentIconWikitext = tournamentIcon ~= '' and
('[[File:' .. tournamentIcon .. '|25px|link=' .. tournamentPage .. ']]\t') or ''
table.insert(nextMatchElements,
Span{
classes = {'tp-tournament'},
children = {'<br>' .. tournamentIconWikitext .. '[[' .. tournamentPage .. '|' .. tournamentLinkText .. ']]'}
}
)
end
if cdHtml ~= '' then
table.insert(nextMatchElements,
Span{classes = {'tp-countdown'}, children = {'<br>' .. cdHtml}}
)
end
else
table.insert(nextMatchElements, Span{classes = {'tp-vs-team'}})
table.insert(nextMatchElements, Span{classes = {'tp-tournament'}})
table.insert(nextMatchElements, Span{classes = {'tp-countdown'}})
end
end


return Div{
return Div{classes = {'tp-next-match'}, children = {Div{classes = {'tp-sub-title'}, children = elements}}}
classes = {'tp-next-match'},
children = {
Div{classes = {'tp-sub-title'}, children = nextMatchElements}
}
}
end
end


-- Builds the latest transfer section for a team
---Builds the latest transfer section.
-- @param teamNameUc string Uppercase team name
---@param teamName string
-- @param lang table Language object for date formatting
---@param lang table
-- @return table HTML container for latest transfer section
---@return WidgetDiv
function TeamPortal._buildLatestTransferSection(teamNameUc, lang)
function TeamPortal._buildLatestTransferSection(teamName, lang)
-- Fetch latest transfer data
local teamNameUc = teamName:gsub('^%l', mw.ustring.upper)
local transferData = mw.ext.LiquipediaDB.lpdb('transfer', {
local transferData = Lpdb.call('transfer', {
limit = 1,
limit = 1,
offset = 0,
conditions = '[[fromteam::' .. teamNameUc .. ']] OR [[toteam::' .. teamNameUc .. ']]',
conditions = '[[fromteam::' .. teamNameUc .. ']] OR [[toteam::' .. teamNameUc .. ']]',
order = 'date desc',
order = 'date desc',
query = 'player, nationality, fromteam, toteam, role1, role2, date, extradata',
query = 'player, nationality, fromteam, toteam, role1, role2, date, extradata',
})
})[1]


local transferText = ''
local transferText = ''
if transferData[1] then
if transferData then
local transferPlayer = transferData[1].player or ''
local player = transferData.player or ''
local transferId = transferData[1].extradata and transferData[1].extradata.displayname or transferPlayer
local displayId = (transferData.extradata and transferData.extradata.displayname) or player
local transferFlag = transferData[1].nationality or ''
local date = transferData.date and lang:formatDate('F j, Y', transferData.date) or ''
local transferFromTeam = transferData[1].fromteam or ''
local transferToTeam = transferData[1].toteam or ''
local transferRole1 = transferData[1].role1 or ''
local transferRole2 = transferData[1].role2 or ''
local transferDate = transferData[1].date or ''
local transferDateFormatted = transferDate ~= '' and lang:formatDate('F j, Y', transferDate) or ''


if transferPlayer ~= '' and transferDateFormatted ~= '' then
if player ~= '' and date ~= '' then
local transferPlayerPageData = mw.ext.LiquipediaDB.lpdb('player', {
local flagIcon = transferData.nationality and Flags.Icon({ flag = transferData.nationality, shouldLink = true }) or ''
conditions = '[[pageid::!0]] AND ([[pagename::' .. (transferId or transferPlayer) .. ']] OR [[pagename::' .. (transferId or transferPlayer):gsub(' ', '_') .. ']])',
local playerLink = Link{page = displayId, children = {player}}
query = 'pagename',
limit = 1
if teamNameUc == transferData.fromteam then
})
local role = transferData.role1 and (' (' .. transferData.role1 .. ')') or ''
local transferPlayerPageExists = transferPlayerPageData[1] ~= nil
transferText = {'<br>', flagIcon, '\t', playerLink, '\tleaves on\t', date, role}
 
elseif teamNameUc == transferData.toteam then
local transferFlagIcon = transferFlag ~= '' and Flags.Icon({ flag = transferFlag, shouldLink = transferPlayerPageExists }) or ''
local role = transferData.role2 and (' (' .. transferData.role2 .. ')') or ''
local playerLinkOrText = transferPlayerPageExists
transferText = {'<br>', flagIcon, '\t', playerLink, '\tjoins on\t', date, role}
and '[[' .. (transferId or transferPlayer) .. '|' .. transferPlayer .. ']]'
or transferPlayer
 
if teamNameUc == transferFromTeam then
transferText = '<br>' .. transferFlagIcon .. (transferFlagIcon ~= '' and '\t' or '')
.. playerLinkOrText .. '\tleaves on\t' .. transferDateFormatted
if transferRole1 ~= '' then
transferText = transferText .. '\t(' .. transferRole1 .. ')'
end
elseif teamNameUc == transferToTeam then
transferText = '<br>' .. transferFlagIcon .. (transferFlagIcon ~= '' and '\t' or '')
.. playerLinkOrText .. '\tjoins on\t' .. transferDateFormatted
if transferRole2 ~= '' then
transferText = transferText .. '\t(' .. transferRole2 .. ')'
end
end
end
end
end
Line 416: Line 212:
classes = {'tp-latest-transfer'},
classes = {'tp-latest-transfer'},
children = {
children = {
Div{
Div{classes = {'tp-sub-title'}, children = {
classes = {'tp-sub-title'},
'Latest Transfer',
children = {
Span{classes = {'tp-transfer'}, children = transferText}
'Latest Transfer',
}}
Span{classes = {'tp-transfer'}, children = {transferText}}
}
}
}
}
end
 
---Builds the navigation button row.
---@param teamPageName string
---@return WidgetDiv
function TeamPortal._buildButtonRow(teamPageName)
return Div{
classes = {'tp-button-row'},
children = {
Button{page = teamPageName, classes = {'tp-button'}, children = {'Team Page'}},
Button{page = teamPageName .. '/Results', classes = {'tp-button'}, children = {'Results'}},
Button{page = teamPageName .. '/Played Matches', classes = {'tp-button'}, children = {'Played Matches'}},
}
}
}
}

Revision as of 19:16, 23 October 2025

Documentation for this module may be created at Module:ArchiveTeam portal list/doc

local Lua = require('Module:Lua')
local Arguments = require('Module:Arguments')
local Lpdb = require('Module:Lpdb')

local Countdown = Lua.import('Module:Countdown')
local DateExt = Lua.import('Module:Date/Ext')
local Flags = Lua.import('Module:Flags')

local Widgets = Lua.import('Module:Widget/Html/All')
local Div = Widgets.Div
local Span = Widgets.Span
local Link = Lua.import('Module:Widget/Basic/Link')
local Button = Lua.import('Module:Widget/Basic/Button')
local Image = Lua.import('Module:Widget/Image')

local TeamPortal = {}

local config = {
	MAX_PLAYERS = 8,
	DEFAULT_TEAMS = 10,
	BLANK_PLAYER_IMAGE = 'Blank Player Image nopadding.png'
}

---Sanitizes a string for use as an HTML class name.
---@param class string
---@return string
function TeamPortal._sanitizeClass(class)
	class = class:gsub('%s', '_')
	class = mw.uri.encode(class, 'PATH')
	class = class:gsub('[^A-Za-z0-9%-_]', function(c)
		return string.format('.%02x', string.byte(c))
	end)
	if class:match('^[%d]') or class:match('^%-[%d]') then
		class = '_' .. class
	end
	return class
end

---Creates and renders the team portal.
---@param frame table
---@return string
function TeamPortal.create(frame)
	local args = Arguments.getArgs(frame)
	local lang = mw.language.getContentLanguage()

	local lpdbData = Lpdb.call('team', {
		limit = args.teams or config.DEFAULT_TEAMS,
		conditions = '[[pageid::!0]] AND [[disbanddate::' .. DateExt.defaultDate .. ']]',
		order = 'earnings desc',
		query = 'name, pagename, earnings',
	})

	local portalItems = {}
	for i, teamData in ipairs(lpdbData) do
		local teamName = teamData.name
		local teamPageName = teamData.pagename
		local teamEarnings = lang:formatNum(tonumber(teamData.earnings))
		local sanitizedTeamId = TeamPortal._sanitizeClass(teamPageName)

		-- Create card title
		local cardTitle = Div{
			classes = {'tp-rank-card-title', 'mw-customtoggle-' .. sanitizedTeamId},
			children = {
				Span{classes = {'tp-rank-number'}, children = {'#' .. i}},
				mw.ext.TeamTemplate.teamicon(teamName),
				Span{classes = {'tp-team-name'}, children = {teamName}},
				Span{classes = {'tp-total-earnings'}, children = {
					Span{classes = {'tp-total-earnings-text'}, children = {'Total Earnings:'}},
					' $' .. teamEarnings
				}}
			}
		}

		-- Build collapsible content sections
		local playerContainer = TeamPortal._buildPlayerSection(teamPageName)
		local nextMatchSection = TeamPortal._buildNextMatchSection(teamName)
		local latestTransferSection = TeamPortal._buildLatestTransferSection(teamName, lang)
		local buttonRow = TeamPortal._buildButtonRow(teamPageName)

		-- Assemble the team box
		local teamBox = Div{
			classes = {'tp-team-box', 'mw-collapsible'},
			attributes = {id = 'mw-customcollapsible-' .. sanitizedTeamId},
			children = {playerContainer, nextMatchSection, latestTransferSection, buttonRow}
		}
		
		if i ~= 1 then
			table.insert(teamBox.props.classes, 'mw-collapsed')
		end

		local card = Div{classes = {'tp-rank-card'}, children = {cardTitle, teamBox}}
		table.insert(portalItems, Div{classes = {'team-portal-list'}, children = {card}})
	end

	return tostring(Div{children = portalItems}:render())
end

---Builds the player display section.
---@param teamPageName string
---@return WidgetDiv
function TeamPortal._buildPlayerSection(teamPageName)
	local squadData = Lpdb.call('squadplayer', {
		limit = config.MAX_PLAYERS,
		conditions = '[[pagename::' .. teamPageName .. ']] AND [[leavedate::' .. DateExt.defaultDate .. ']] '
			.. 'AND [[inactivedate::' .. DateExt.defaultDate .. ']] AND [[type::player]]',
		order = 'id asc',
		query = 'link, id',
	})

	local playerRows = {}
	for i, squadPlayerData in ipairs(squadData) do
		local playerData = Lpdb.call('player', {
			conditions = '[[pageid::!0]] AND [[pagename::' .. (squadPlayerData.link or ''):gsub(' ', '_') .. ']]',
			query = 'pagename, id, nationality, image',
			limit = 1
		})[1] or {}
		
		local playerPage = playerData.pagename or ''
		local displayId = playerData.id or squadPlayerData.id or ''
		local playerFlag = playerData.nationality or ''

		local playerImage = Image{
			page = playerData.image or config.BLANK_PLAYER_IMAGE,
			width = 186, height = 140,
			link = playerPage ~= '' and playerPage or nil,
		}
		
		local nameWidget = (displayId ~= '' and playerPage ~= '') and Link{page = playerPage, children = {displayId}} or displayId
		local flagIcon = playerFlag ~= '' and Flags.Icon({ flag = playerFlag, shouldLink = playerPage ~= '' }) or ''

		table.insert(playerRows, Div{
			classes = {'tp-player-' .. i},
			css = {['text-align'] = 'center', ['margin-bottom'] = '2px'},
			children = {
				Div{classes = {'tp-photo'}, css = {['margin-bottom'] = '5px'}, children = {playerImage}},
				Div{classes = {'tp-name'}, children = {flagIcon, (flagIcon ~= '' and nameWidget ~= '') and '\t' or '', nameWidget}}
			}
		})
	end
	
	return Div{
		css = {
			display = 'flex', ['flex-wrap'] = 'wrap', ['justify-content'] = 'center',
			['align-items'] = 'flex-start', gap = '15px', padding = '5px 0'
		},
		children = playerRows
	}
end

---Builds the next match section.
---@param teamName string
---@return WidgetDiv
function TeamPortal._buildNextMatchSection(teamName)
	local matchData = Lpdb.call('match', {
		limit = 1,
		conditions = '[[pageid::!-1]] AND [[finished::0]] AND [[dateexact::1]] '
			.. 'AND ([[opponent1::' .. teamName .. ']] OR [[opponent2::' .. teamName .. ']])',
		order = 'date asc',
		query = 'pagename, tickername, opponent1, opponent2, date',
	})[1]

	local elements = {'Next Match'}
	if matchData then
		local opponent = (teamName == matchData.opponent1) and matchData.opponent2 or matchData.opponent1
		table.insert(elements, Span{classes = {'tp-vs-team'}, children = {'<br>' .. (opponent ~= 'TBD' and mw.ext.TeamTemplate.team(opponent) or 'TBD')}})
		
		local tourneyLink = Link{page = matchData.pagename, children = {matchData.tickername or matchData.pagename}}
		table.insert(elements, Span{classes = {'tp-tournament'}, children = {'<br>', tourneyLink}})
		
		if matchData.date then
			table.insert(elements, Span{classes = {'tp-countdown'}, children = {'<br>', Countdown.create({ date = matchData.date })}})
		end
	end

	return Div{classes = {'tp-next-match'}, children = {Div{classes = {'tp-sub-title'}, children = elements}}}
end

---Builds the latest transfer section.
---@param teamName string
---@param lang table
---@return WidgetDiv
function TeamPortal._buildLatestTransferSection(teamName, lang)
	local teamNameUc = teamName:gsub('^%l', mw.ustring.upper)
	local transferData = Lpdb.call('transfer', {
		limit = 1,
		conditions = '[[fromteam::' .. teamNameUc .. ']] OR [[toteam::' .. teamNameUc .. ']]',
		order = 'date desc',
		query = 'player, nationality, fromteam, toteam, role1, role2, date, extradata',
	})[1]

	local transferText = ''
	if transferData then
		local player = transferData.player or ''
		local displayId = (transferData.extradata and transferData.extradata.displayname) or player
		local date = transferData.date and lang:formatDate('F j, Y', transferData.date) or ''

		if player ~= '' and date ~= '' then
			local flagIcon = transferData.nationality and Flags.Icon({ flag = transferData.nationality, shouldLink = true }) or ''
			local playerLink = Link{page = displayId, children = {player}}
			
			if teamNameUc == transferData.fromteam then
				local role = transferData.role1 and (' (' .. transferData.role1 .. ')') or ''
				transferText = {'<br>', flagIcon, '\t', playerLink, '\tleaves on\t', date, role}
			elseif teamNameUc == transferData.toteam then
				local role = transferData.role2 and (' (' .. transferData.role2 .. ')') or ''
				transferText = {'<br>', flagIcon, '\t', playerLink, '\tjoins on\t', date, role}
			end
		end
	end

	return Div{
		classes = {'tp-latest-transfer'},
		children = {
			Div{classes = {'tp-sub-title'}, children = {
				'Latest Transfer',
				Span{classes = {'tp-transfer'}, children = transferText}
			}}
		}
	}
end

---Builds the navigation button row.
---@param teamPageName string
---@return WidgetDiv
function TeamPortal._buildButtonRow(teamPageName)
	return Div{
		classes = {'tp-button-row'},
		children = {
			Button{page = teamPageName, classes = {'tp-button'}, children = {'Team Page'}},
			Button{page = teamPageName .. '/Results', classes = {'tp-button'}, children = {'Results'}},
			Button{page = teamPageName .. '/Played Matches', classes = {'tp-button'}, children = {'Played Matches'}},
		}
	}
end

return TeamPortal