Module:PlayerDashboard: Difference between revisions
From eSportsAmaze
More actions
Esportsamaze (talk | contribs) No edit summary |
Esportsamaze (talk | contribs) No edit summary |
||
| (9 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
local p = {} | local p = {} | ||
local cargo = mw.ext.cargo | local cargo = mw.ext.cargo | ||
local html = mw.html | local html = mw.html | ||
local lang = mw.getContentLanguage() | local lang = mw.getContentLanguage() | ||
-- ── Helpers ────────────────────────────────────────────────────── | |||
local function esc(s) | local function esc(s) | ||
if not s then return "" end | if not s then return "" end | ||
return s:gsub("\\","\\\\"):gsub("'","\\'") | return s:gsub("\\", "\\\\"):gsub("'", "\\'") | ||
end | end | ||
local function fmtCurrency(n) | local function fmtCurrency(n) | ||
if not n or n == 0 then return "—" end | if not n or n == 0 then return "—" end | ||
local | local num = math.floor(tonumber(n) or 0) | ||
local | local s = tostring(num) | ||
local | if #s <= 3 then return "₹ " .. s end | ||
local result = s:sub(-3) | |||
local remaining = s:sub(1, -4) | |||
return "₹ " .. | while #remaining > 2 do | ||
result = remaining:sub(-2) .. "," .. result | |||
remaining = remaining:sub(1, -3) | |||
end | |||
return "₹ " .. remaining .. "," .. result | |||
end | end | ||
| Line 28: | Line 30: | ||
end | end | ||
local function | local function getTierClass(tier) | ||
if not tier then return "tier-def" end | |||
local t = tier:lower() | |||
if t:find("s") and t:find("tier") then return "tier-s" end | |||
if t:find("a") and t:find("tier") then return "tier-a" end | |||
if t:find("b") and t:find("tier") then return "tier-b" end | |||
if t:find("c") and t:find("tier") then return "tier-c" end | |||
return "tier-def" | |||
end | |||
local FLAGS = { | |||
india="🇮🇳", bangladesh="🇧🇩", pakistan="🇵🇰", nepal="🇳🇵", | |||
srilanka="🇱🇰", myanmar="🇲🇲", indonesia="🇮🇩", thailand="🇹🇭", | |||
malaysia="🇲🇾", philippines="🇵🇭", vietnam="🇻🇳", singapore="🇸🇬", | |||
china="🇨🇳", japan="🇯🇵", korea="🇰🇷", usa="🇺🇸", | |||
uk="🇬🇧", germany="🇩🇪", brazil="🇧🇷", australia="🇦🇺" | |||
} | |||
local function getFlag(country) | |||
if not country or country == "" then return "" end | |||
local key = country:lower():gsub("%s+",""):gsub("-","") | |||
return (FLAGS[key] or "") .. " " | |||
end | |||
local function statusStyle(s) | |||
s = (s or ""):lower() | s = (s or ""):lower() | ||
if s == "active" then return "# | if s == "active" then return "#4ade80","rgba(34,197,94,.2)", "rgba(34,197,94,.35)" end | ||
if s == "inactive" then return "# | if s == "inactive" then return "#fbbf24","rgba(251,191,36,.2)", "rgba(251,191,36,.35)" end | ||
if s == "retired" then return "#94a3b8", "rgba(148,163,184,. | if s == "retired" then return "#94a3b8","rgba(148,163,184,.2)","rgba(148,163,184,.35)" end | ||
if s == "banned" then return "# | if s == "banned" then return "#f87171","rgba(239,68,68,.2)", "rgba(239,68,68,.35)" end | ||
return "#94a3b8", "rgba(148,163,184,. | return "#94a3b8","rgba(148,163,184,.2)","rgba(148,163,184,.35)" | ||
end | |||
-- ── Team info + logo helpers ────────────────────────────────────── | |||
local teamInfoCache = {} | |||
local function getTeamInfo(teamName) | |||
if not teamName or teamName == "" then return nil end | |||
if teamInfoCache[teamName] then return teamInfoCache[teamName] end | |||
local rows = cargo.query("Teams", | |||
"display_name,image,image_dark", | |||
{ where = "name='" .. esc(teamName) .. "'", limit = 1 }) | |||
local result = (rows and #rows > 0) and rows[1] or nil | |||
teamInfoCache[teamName] = result | |||
return result | |||
end | |||
-- Outputs both light + dark logo, CSS switches via logo-lightmode/logo-darkmode | |||
local function teamLogoWikitext(ti, size) | |||
if not ti then return nil end | |||
size = size or "26px" | |||
local light = (ti.image and ti.image ~= "") and ti.image or nil | |||
local dark = (ti.image_dark and ti.image_dark ~= "") and ti.image_dark or light | |||
if not light then return nil end | |||
return "[[File:"..light.."|"..size.."|link=|class=pd-team-logo-img logo-lightmode]]" | |||
.. "[[File:"..dark .."|"..size.."|link=|class=pd-team-logo-img logo-darkmode]]" | |||
end | |||
-- Place badge matching achievements table style | |||
local function renderPlaceBadge(td, placeNum) | |||
local badgeClass = "place-def" | |||
local placeText = tostring(placeNum) .. "th" | |||
if placeNum == 1 then badgeClass = "place-1"; placeText = "1st" | |||
elseif placeNum == 2 then badgeClass = "place-2"; placeText = "2nd" | |||
elseif placeNum == 3 then badgeClass = "place-3"; placeText = "3rd" | |||
end | |||
td:tag('span'):addClass('place-badge ' .. badgeClass):wikitext(placeText) | |||
end | end | ||
-- ── Main ───────────────────────────────────────────────────────── | |||
function p.main(frame) | function p.main(frame) | ||
local pageName | local pageName = mw.title.getCurrentTitle().text | ||
local sqlPage | local sqlPage = esc(pageName) | ||
-- ── 1. | -- ── 1. Player row ───────────────────────────────────────────── | ||
local pRows = cargo.query("Players", | local pRows = cargo.query("Players", | ||
"id,real_name,image,current_team,nationality,status,game,birth_date,role, | "id,real_name,image,current_team,nationality,status,game," | ||
.. "birth_date,role,instagram,youtube,twitter,discord,facebook", | |||
{ where = "_pageName='" .. sqlPage .. "'", limit = 1 }) | { where = "_pageName='" .. sqlPage .. "'", limit = 1 }) | ||
local pl = (pRows and #pRows > 0) and pRows[1] or {} | local pl = (pRows and #pRows > 0) and pRows[1] or {} | ||
local displayName = pl.id or mw.title.getCurrentTitle().subpageText | local displayName = pl.id or mw.title.getCurrentTitle().subpageText | ||
local sqlName = esc(displayName) | |||
-- ── 2. Krafton rank | -- ── 2. Krafton rank ─────────────────────────────────────────── | ||
local rankVal = | local rankVal = nil | ||
local rRows = cargo.query("Krafton_Rankings", "rank", { | local rRows = cargo.query("Krafton_Rankings", "rank", { | ||
where = string.format("(name='%s' OR name='%s') AND type='Player'", | where = string.format( | ||
"(name='%s' OR name='%s') AND type='Player'", sqlPage, sqlName), | |||
limit = 1 }) | |||
if rRows and #rRows > 0 then rankVal = "#" .. rRows[1].rank end | if rRows and #rRows > 0 then rankVal = "#" .. rRows[1].rank end | ||
-- ── 3. Earnings | -- ── 3. All tournaments player appeared in ───────────────────── | ||
local participation = cargo.query("Tournament_Teams", | |||
"tournament,team,p1,p2,p3,p4,p5,p6", | |||
{ where = string.format( | |||
"p1='%s' OR p1='%s' OR p2='%s' OR p2='%s' OR " | |||
.."p3='%s' OR p3='%s' OR p4='%s' OR p4='%s' OR " | |||
.."p5='%s' OR p5='%s' OR p6='%s' OR p6='%s'", | |||
sqlPage,sqlName, sqlPage,sqlName, sqlPage,sqlName, | |||
sqlPage,sqlName, sqlPage,sqlName, sqlPage,sqlName), | |||
limit = 500 }) | |||
participation = participation or {} | |||
-- Tournament -> team lookup (for individual awards team column) | |||
local tournTeamMap = {} | |||
for _, r in ipairs(participation) do | |||
if r.tournament and r.team and not tournTeamMap[r.tournament] then | |||
tournTeamMap[r.tournament] = r.team | |||
end | |||
end | |||
-- ── 4. Teammates ────────────────────────────────────────────── | |||
-- Count how many tournaments each co-player appeared in with this player | |||
-- Skip coach/analyst — only p1-p6. Skip the player himself. | |||
local teammateCount = {} | |||
local slots = {"p1","p2","p3","p4","p5","p6"} | |||
for _, r in ipairs(participation) do | |||
for _, slot in ipairs(slots) do | |||
local name = r[slot] | |||
if name and name ~= "" and name ~= pageName and name ~= displayName then | |||
teammateCount[name] = (teammateCount[name] or 0) + 1 | |||
end | |||
end | |||
end | |||
-- Sort teammates by frequency desc, take top 10 | |||
local teammates = {} | |||
for name, count in pairs(teammateCount) do | |||
table.insert(teammates, { name = name, count = count }) | |||
end | |||
table.sort(teammates, function(a, b) return a.count > b.count end) | |||
if #teammates > 10 then | |||
local trimmed = {} | |||
for i = 1, 10 do trimmed[i] = teammates[i] end | |||
teammates = trimmed | |||
end | |||
-- ── 5. Earnings ─────────────────────────────────────────────── | |||
local indEarnings, teamEarnings = 0, 0 | local indEarnings, teamEarnings = 0, 0 | ||
local | local eRows = cargo.query("PrizeMoney", "SUM(prize)=total", { | ||
where = string.format( | where = string.format( | ||
" | "(player='%s' OR player='%s') AND player != ''", sqlPage, sqlName) }) | ||
if eRows and #eRows > 0 then | |||
indEarnings = tonumber(eRows[1].total) or 0 | |||
end | |||
if | if #participation > 0 then | ||
local conds = {} | local conds = {} | ||
for _, r in ipairs(participation) do | for _, r in ipairs(participation) do | ||
if r.tournament and r.team then | if r.tournament and r.team then | ||
table.insert(conds, string.format( | table.insert(conds, string.format( | ||
"(tournament='%s' AND team='%s')", esc(r.tournament), esc(r.team))) | "(tournament='%s' AND team='%s')", | ||
esc(r.tournament), esc(r.team))) | |||
end | end | ||
end | end | ||
if #conds > 0 then | if #conds > 0 then | ||
local tPrizes = cargo.query("PrizeMoney", "SUM(prize)=team_total", { | local tPrizes = cargo.query("PrizeMoney", "SUM(prize)=team_total", { | ||
where = "("..table.concat(conds," OR ")..")" .. | where = "(" .. table.concat(conds, " OR ") .. ")" | ||
.. " AND (player='' OR player IS NULL)" }) | |||
if tPrizes and #tPrizes > 0 then | if tPrizes and #tPrizes > 0 then | ||
teamEarnings = tonumber(tPrizes[1].team_total) or 0 | teamEarnings = tonumber(tPrizes[1].team_total) or 0 | ||
| Line 91: | Line 197: | ||
local totalEarnings = indEarnings + teamEarnings | local totalEarnings = indEarnings + teamEarnings | ||
-- ── | -- ── 6. Team history ─────────────────────────────────────────── | ||
local teamHist = cargo.query("Player_Former_Teams", | local teamHist = cargo.query("Player_Former_Teams", | ||
"team,join_date,leave_date", | "team,join_date,leave_date", | ||
{ where = "player_id='"..sqlPage.."'", orderBy="join_date DESC", limit= | { where = "player_id='" .. sqlPage .. "'", | ||
if #teamHist == 0 then | orderBy = "join_date DESC", limit = 20 }) | ||
if not teamHist or #teamHist == 0 then | |||
teamHist = cargo.query("Player_Former_Teams", | teamHist = cargo.query("Player_Former_Teams", | ||
"team,join_date,leave_date", | "team,join_date,leave_date", | ||
{ where="player_id='".. | { where = "player_id='" .. sqlName .. "'", | ||
orderBy = "join_date DESC", limit = 20 }) | |||
end | end | ||
teamHist = teamHist or {} | |||
-- ── | -- ── 7a. Individual awards ───────────────────────────────────── | ||
local indAwards = cargo.query("PrizeMoney", | |||
"tournament,award,prize", | |||
{ where = string.format( | |||
"(player='%s' OR player='%s') AND player != ''", | |||
sqlPage, sqlName), | |||
orderBy = "tournament DESC", limit = 100 }) | |||
indAwards = indAwards or {} | |||
if | -- ── 7b. Team tournament results ─────────────────────────────── | ||
local | local teamResults = {} | ||
local seenKey = {} | |||
if #participation > 0 then | |||
local pairs_list = {} | |||
for _, r in ipairs(participation) do | for _, r in ipairs(participation) do | ||
if r.tournament and r.team then | if r.tournament and r.team then | ||
table.insert( | local k = r.tournament .. "|" .. r.team | ||
if not seenKey[k] then | |||
seenKey[k] = true | |||
table.insert(pairs_list, { tournament = r.tournament, team = r.team }) | |||
end | |||
end | end | ||
end | end | ||
for _, | if #pairs_list > 0 then | ||
local ttConds = {} | |||
for _, r in ipairs(pairs_list) do | |||
table.insert(ttConds, string.format( | |||
"(tournament='%s' AND team='%s')", | |||
esc(r.tournament), esc(r.team))) | |||
end | |||
local whereStr = table.concat(ttConds, " OR ") | |||
local placeIndex = {} | |||
local placeRows = cargo.query("Tournament_Results", | |||
"tournament,team,place", | |||
{ where = whereStr, limit = 500 }) | |||
if placeRows then | |||
for _, row in ipairs(placeRows) do | |||
placeIndex[row.tournament .. "|" .. row.team] = tonumber(row.place) | |||
end | |||
end | |||
local prizeIndex = {} | |||
local prizeRows = cargo.query("PrizeMoney", | |||
"tournament,team,prize,placement,award", | |||
{ where = "(" .. whereStr .. ")" | |||
.. " AND (player='' OR player IS NULL)", | |||
local | limit = 500 }) | ||
if prizeRows then | |||
for _, row in ipairs(prizeRows) do | |||
local k = row.tournament .. "|" .. row.team | |||
local newPrize = tonumber(row.prize) or 0 | |||
local existing = prizeIndex[k] | |||
if not existing or newPrize > (existing.prize or 0) then | |||
prizeIndex[k] = { | |||
prize = newPrize, | |||
placement = row.placement or "", | |||
award = row.award or "", | |||
} | |||
end | |||
end | |||
end | |||
for _, r in ipairs(pairs_list) do | |||
local k = r.tournament .. "|" .. r.team | |||
local pr = prizeIndex[k] | |||
local pl_num = placeIndex[k] | |||
if not pl_num and pr and pr.placement ~= "" then | |||
local n = pr.placement:match("(%d+)") | |||
if n then pl_num = tonumber(n) end | |||
end | end | ||
table.insert(teamResults, { | |||
tournament = r.tournament, | |||
team = r.team, | |||
place = pl_num, | |||
award = (pr and pr.award ~= "" and pr.award) or nil, | |||
prize = (pr and pr.prize > 0 and pr.prize) or 0, | |||
}) | |||
end | |||
end | |||
end | |||
table.sort(teamResults, function(a, b) | |||
if a.place and b.place then return a.place < b.place end | |||
if a.place then return true end | |||
if b.place then return false end | |||
return (a.tournament or "") > (b.tournament or "") | |||
end) | |||
-- ── 8. Tournament metadata ──────────────────────────────────── | |||
local allTournNames = {} | |||
local tournSeen = {} | |||
local function addTourn(name) | |||
if name and name ~= "" and not tournSeen[name] then | |||
tournSeen[name] = true | |||
table.insert(allTournNames, "'" .. esc(name) .. "'") | |||
end | |||
end | |||
for _, a in ipairs(indAwards) do addTourn(a.tournament) end | |||
for _, t in ipairs(teamResults) do addTourn(t.tournament) end | |||
local tournMeta = {} | |||
if #allTournNames > 0 then | |||
local metaRows = cargo.query("Tournaments", "name,tier,end_date", { | |||
where = "name IN (" .. table.concat(allTournNames, ",") .. ")", | |||
limit = 200 }) | |||
if metaRows then | |||
for _, row in ipairs(metaRows) do | |||
tournMeta[row.name] = { tier = row.tier, date = row.end_date } | |||
end | end | ||
end | end | ||
end | end | ||
-- ── | -- ── BUILD HTML ──────────────────────────────────────────────── | ||
local root = html.create('div'):addClass('pd-dashboard') | local sc, sbg, sborder = statusStyle(pl.status) | ||
local initials = displayName:sub(1, 2):upper() | |||
local root = html.create('div'):addClass('pd-dashboard') | |||
-- ════ HERO ════ | |||
-- Layout: [photo] [name + realname + tags] [earnings — hidden on mobile] | |||
-- Krafton rank is NO LONGER here — moved to left info panel | |||
local hero = root:tag('div'):addClass('pd-hero') | local hero = root:tag('div'):addClass('pd-hero') | ||
local | -- Photo | ||
local photo = hero:tag('div'):addClass('pd-hero-photo') | |||
if pl.image and pl.image ~= "" then | if pl.image and pl.image ~= "" then | ||
photo:wikitext("[[File:" .. pl.image .. "|120px|link=|class=pd-hero-img]]") | |||
else | else | ||
photo:wikitext(initials) | |||
end | end | ||
-- Name + tags (no rank chip here anymore) | |||
local heroInfo = hero:tag('div'):addClass('pd-hero-info') | local heroInfo = hero:tag('div'):addClass('pd-hero-info') | ||
heroInfo:tag('div'):addClass('pd-hero-name'):wikitext(displayName) | heroInfo:tag('div'):addClass('pd-hero-name'):wikitext(displayName) | ||
| Line 178: | Line 351: | ||
heroInfo:tag('div'):addClass('pd-hero-realname'):wikitext(pl.real_name) | heroInfo:tag('div'):addClass('pd-hero-realname'):wikitext(pl.real_name) | ||
end | end | ||
local tags = heroInfo:tag('div'):addClass('pd-hero-tags') | local tags = heroInfo:tag('div'):addClass('pd-hero-tags') | ||
if pl.status and pl.status ~= "" then | if pl.status and pl.status ~= "" then | ||
tags:tag('span'):addClass('pd-tag') | tags:tag('span'):addClass('pd-tag') | ||
:css('color', sc):css('background', sbg):css('border', '1px solid '..sborder) | :css('color', sc):css('background', sbg) | ||
:css('border', '1px solid ' .. sborder) | |||
:wikitext('● ' .. pl.status) | :wikitext('● ' .. pl.status) | ||
end | end | ||
| Line 190: | Line 363: | ||
if pl.game and pl.game ~= "" then | if pl.game and pl.game ~= "" then | ||
tags:tag('span'):addClass('pd-tag pd-tag-game'):wikitext(pl.game) | tags:tag('span'):addClass('pd-tag pd-tag-game'):wikitext(pl.game) | ||
end | end | ||
-- Earnings: right side on desktop, separate row on mobile via CSS | |||
-- pd-earnings-total wraps label+number so flex can split total vs breakdown | |||
if totalEarnings > 0 then | if totalEarnings > 0 then | ||
local | local er = hero:tag('div'):addClass('pd-hero-right') | ||
local et = er:tag('div'):addClass('pd-earnings-total') | |||
et:tag('div'):addClass('pd-earnings-label'):wikitext('Approx. Earnings') | |||
local bd = | et:tag('div'):addClass('pd-earnings-val'):wikitext(fmtCurrency(totalEarnings)) | ||
bd:tag('div | local bd = er:tag('div'):addClass('pd-earnings-breakdown') | ||
bd:tag('div') | |||
:tag('div'):addClass('pd-earn-sub-label'):wikitext('Team'):done() | :tag('div'):addClass('pd-earn-sub-label'):wikitext('Team'):done() | ||
:tag('div'):addClass('pd-earn-sub-val'):wikitext(fmtCurrency(teamEarnings)) | :tag('div'):addClass('pd-earn-sub-val'):wikitext(fmtCurrency(teamEarnings)) | ||
bd:tag('div | bd:tag('div') | ||
:tag('div'):addClass('pd-earn-sub-label'):wikitext('Individual'):done() | :tag('div'):addClass('pd-earn-sub-label'):wikitext('Individual'):done() | ||
:tag('div'):addClass('pd-earn-sub-val'):wikitext(fmtCurrency(indEarnings)) | :tag('div'):addClass('pd-earn-sub-val'):wikitext(fmtCurrency(indEarnings)) | ||
end | end | ||
-- | -- ════ BODY ════ | ||
local body = root:tag('div'):addClass('pd-body') | local body = root:tag('div'):addClass('pd-body') | ||
local left = body:tag('div'):addClass('pd-left') | local left = body:tag('div'):addClass('pd-left') | ||
left:tag('div'):addClass('pd-section-title'):wikitext('Player Info') | left:tag('div'):addClass('pd-section-title'):wikitext('Player Info') | ||
local function infoRow(label, val | local function infoRow(label, val) | ||
if not val or val == "" then return end | if not val or val == "" then return end | ||
local row = left:tag('div'):addClass('pd-info-row') | left:tag('div'):addClass('pd-info-row') | ||
row:tag('span'):addClass('pd-info-label'):wikitext( | :tag('span'):addClass('pd-info-label'):wikitext(label):done() | ||
local | :tag('span'):addClass('pd-info-val'):wikitext(val) | ||
if | end | ||
infoRow('Real Name', pl.real_name) | |||
if pl.birth_date and pl.birth_date ~= "" then | |||
infoRow('Born', lang:formatDate("d F Y", pl.birth_date)) | |||
end | |||
if pl.nationality and pl.nationality ~= "" then | |||
infoRow('Nationality', getFlag(pl.nationality) .. pl.nationality) | |||
end | |||
infoRow('Role', pl.role) | |||
infoRow('Game', pl.game) | |||
-- Current team: logo + display_name | |||
if pl.current_team and pl.current_team ~= "" then | |||
local ti = getTeamInfo(pl.current_team) | |||
local teamDisplay = (ti and ti.display_name and ti.display_name ~= "") | |||
and ti.display_name or pl.current_team | |||
local logoHtml = teamLogoWikitext(ti, "22px") | |||
local row = left:tag('div'):addClass('pd-info-row') | |||
row:tag('span'):addClass('pd-info-label'):wikitext('Current Team') | |||
local wrap = row:tag('span'):addClass('pd-info-val') | |||
:tag('span'):addClass('pd-current-team') | |||
if logoHtml then wrap:wikitext(logoHtml) end | |||
wrap:tag('span'):addClass('pd-current-team-name') | |||
:wikitext("[[" .. pl.current_team .. "|" .. teamDisplay .. "]]") | |||
end | end | ||
-- Krafton rank now sits naturally in the info panel | |||
if rankVal then infoRow('Krafton Rank', rankVal) end | |||
infoRow('Krafton Rank',rankVal | |||
-- Socials | -- Socials | ||
local socials = { | local socials = { | ||
{k='instagram', file='Icon_instagram.png', base='https://instagram.com/'}, | { k='instagram', file='Icon_instagram.png', base='https://instagram.com/' }, | ||
{k='twitter', file='Icon_twitter.png', base='https://twitter.com/'}, | { k='twitter', file='Icon_twitter.png', base='https://twitter.com/' }, | ||
{k='youtube', file='Icon_youtube.png', base='https://youtube.com/'}, | { k='youtube', file='Icon_youtube.png', base='https://youtube.com/' }, | ||
{k='discord', file='Icon_discord.png', base='https://discord.gg/'}, | { k='discord', file='Icon_discord.png', base='https://discord.gg/' }, | ||
{k='facebook', file='Icon_facebook.png', base='https://facebook.com/'}, | { k='facebook', file='Icon_facebook.png', base='https://facebook.com/' }, | ||
} | } | ||
local | local socDiv = left:tag('div'):addClass('pd-socials') | ||
for _, s in ipairs(socials) do | for _, s in ipairs(socials) do | ||
local v = pl[s.k] | local v = pl[s.k] | ||
if v and v ~= "" then | if v and v ~= "" then | ||
local url = v:match("^https?://") and v or (s.base .. v) | local url = v:match("^https?://") and v or (s.base .. v) | ||
socDiv:wikitext("[[File:" .. s.file | |||
.. "|32px|link=" .. url .. "|class=social-img]]") | |||
end | end | ||
end | end | ||
| Line 257: | Line 442: | ||
-- Team history | -- Team history | ||
if #teamHist > 0 then | if #teamHist > 0 then | ||
left:tag('div'):addClass('pd-section-title pd-section | left:tag('div'):addClass('pd-section-title pd-section-gap') | ||
:wikitext('Team History') | |||
for _, h in ipairs(teamHist) do | for _, h in ipairs(teamHist) do | ||
local ti = getTeamInfo(h.team or "") | |||
local logoHtml = teamLogoWikitext(ti, "24px") | |||
local tr = left:tag('div'):addClass('pd-team-row') | |||
if logoHtml then | |||
tr:wikitext(logoHtml) | |||
else | |||
tr:tag('div'):addClass('pd-team-logo-fb') | |||
:wikitext((h.team or "?"):sub(1, 3):upper()) | |||
end | |||
tr:tag('div'):addClass('pd-team-name') | |||
:wikitext("[[" .. (h.team or "") .. "]]") | |||
tr:tag('div'):addClass('pd-team-dates') | |||
:wikitext(fmtDate(h.join_date) .. " – " | |||
.. (h.leave_date and h.leave_date ~= "" | |||
and fmtDate(h.leave_date) or "Now")) | |||
end | |||
end | |||
-- ── Teammates section ───────────────────────────────────────── | |||
-- Players who shared the most tournaments with this player | |||
-- Source: Tournament_Teams p1-p6, coach/analyst excluded | |||
if #teammates > 0 then | |||
left:tag('div'):addClass('pd-section-title pd-section-gap') | |||
:wikitext('Played With') | |||
for _, tm in ipairs(teammates) do | |||
local tr = left:tag('div'):addClass('pd-team-row') | local tr = left:tag('div'):addClass('pd-team-row') | ||
tr:tag('div'):addClass('pd- | tr:tag('div'):addClass('pd-teammate-name') | ||
:wikitext( | :wikitext("[[" .. tm.name .. "]]") | ||
tr:tag('div'):addClass('pd-teammate-count') | |||
tr:tag('div'):addClass('pd- | :wikitext(tostring(tm.count) | ||
:wikitext( | .. (tm.count == 1 and " tournament" or " tournaments")) | ||
end | end | ||
end | end | ||
-- | -- ════ RIGHT PANEL ════ | ||
local right = body:tag('div'):addClass('pd-right') | local right = body:tag('div'):addClass('pd-right') | ||
if # | local function renderTierCell(td, tier) | ||
local tbl = | if tier and tier ~= "" then | ||
td:addClass('ac-tier') | |||
:tag('span'):addClass('tier-badge ' .. getTierClass(tier)):wikitext(tier) | |||
else | |||
td:wikitext("—") | |||
end | |||
end | |||
local function renderDateCell(td, date) | |||
td:addClass('ac-date'):wikitext(date and date ~= "" and date or "—") | |||
end | |||
-- Individual Awards | |||
if #indAwards > 0 then | |||
local sec = right:tag('div'):addClass('pd-prize-section') | |||
sec:tag('div'):addClass('pd-table-title'):wikitext('🏆 Individual Awards') | |||
local tbl = sec:tag('table'):addClass('pd-tourn-table') | |||
local hdr = tbl:tag('tr') | local hdr = tbl:tag('tr') | ||
for _, | hdr:tag('th'):wikitext('Date') | ||
hdr:tag('th'):wikitext('Tier') | |||
hdr:tag('th'):wikitext('Tournament') | |||
hdr:tag('th'):wikitext('Team') | |||
hdr:tag('th'):wikitext('Award') | |||
hdr:tag('th'):css('text-align','right'):wikitext('Prize') | |||
for _, a in ipairs(indAwards) do | |||
local meta = tournMeta[a.tournament or ""] or {} | |||
local tr = tbl:tag('tr') | |||
renderDateCell(tr:tag('td'), meta.date) | |||
renderTierCell(tr:tag('td'), meta.tier) | |||
tr:tag('td'):css('font-weight','600') | |||
:wikitext("[[" .. (a.tournament or "") .. "]]") | |||
local playerTeam = tournTeamMap[a.tournament or ""] | |||
tr:tag('td'):wikitext(playerTeam and ("[[" .. playerTeam .. "]]") or "—") | |||
tr:tag('td'):tag('span'):addClass('pd-award-badge') | |||
:wikitext(a.award and a.award ~= "" and a.award or "Award") | |||
tr:tag('td'):addClass('ac-prize') | |||
:wikitext(tonumber(a.prize) and tonumber(a.prize) > 0 | |||
and fmtCurrency(tonumber(a.prize)) or "—") | |||
end | end | ||
end | |||
-- Team Tournament Results | |||
local sec2 = right:tag('div'):addClass('pd-prize-section') | |||
sec2:tag('div'):addClass('pd-table-title'):wikitext('🎖 Team Tournament Results') | |||
if #teamResults > 0 then | |||
local | local tbl2 = sec2:tag('table'):addClass('pd-tourn-table') | ||
local hdr2 = tbl2:tag('tr') | |||
hdr2:tag('th'):wikitext('Date') | |||
hdr2:tag('th'):wikitext('Tier') | |||
hdr2:tag('th'):wikitext('Tournament') | |||
hdr2:tag('th'):wikitext('Team') | |||
hdr2:tag('th'):addClass('ac-place'):wikitext('Place/Award') | |||
hdr2:tag('th'):css('text-align','right'):wikitext('Prize') | |||
for _, t in ipairs(teamResults) do | |||
local meta = tournMeta[t.tournament or ""] or {} | |||
local tr2 = tbl2:tag('tr') | |||
renderDateCell(tr2:tag('td'), meta.date) | |||
renderTierCell(tr2:tag('td'), meta.tier) | |||
tr2:tag('td'):css('font-weight','600') | |||
:wikitext("[[" .. t.tournament .. "]]") | |||
tr2:tag('td'):wikitext("[[" .. t.team .. "]]") | |||
local pTd = tr2:tag('td'):addClass('ac-place') | |||
if t.place then | |||
renderPlaceBadge(pTd, t.place) | |||
elseif t.award then | |||
pTd:tag('span'):addClass('pd-award-badge'):wikitext(t.award) | |||
else | |||
pTd:wikitext("—") | |||
end | |||
tr2:tag('td'):addClass('ac-prize') | |||
:wikitext(t.prize > 0 and fmtCurrency(t.prize) or "—") | :wikitext(t.prize > 0 and fmtCurrency(t.prize) or "—") | ||
end | end | ||
else | else | ||
sec2:tag('div'):addClass('pd-empty') | |||
:wikitext('No tournament data found | :wikitext('No tournament data found.') | ||
end | end | ||
right:tag('div'):addClass('pd-stats-placeholder') | right:tag('div'):addClass('pd-stats-placeholder') | ||
:tag('b'):wikitext('📊 Performance Stats'):done() | :tag('b'):wikitext('📊 Performance Stats'):done() | ||
:wikitext('Coming soon — per-match stats will appear here | :wikitext('Coming soon — per-match stats will appear here.') | ||
return tostring(root) | return tostring(root) | ||
Latest revision as of 03:55, 9 March 2026
Documentation for this module may be created at Module:PlayerDashboard/doc
local p = {}
local cargo = mw.ext.cargo
local html = mw.html
local lang = mw.getContentLanguage()
-- ── Helpers ──────────────────────────────────────────────────────
local function esc(s)
if not s then return "" end
return s:gsub("\\", "\\\\"):gsub("'", "\\'")
end
local function fmtCurrency(n)
if not n or n == 0 then return "—" end
local num = math.floor(tonumber(n) or 0)
local s = tostring(num)
if #s <= 3 then return "₹ " .. s end
local result = s:sub(-3)
local remaining = s:sub(1, -4)
while #remaining > 2 do
result = remaining:sub(-2) .. "," .. result
remaining = remaining:sub(1, -3)
end
return "₹ " .. remaining .. "," .. result
end
local function fmtDate(d)
if not d or d == "" then return "?" end
return lang:formatDate("M y", d)
end
local function getTierClass(tier)
if not tier then return "tier-def" end
local t = tier:lower()
if t:find("s") and t:find("tier") then return "tier-s" end
if t:find("a") and t:find("tier") then return "tier-a" end
if t:find("b") and t:find("tier") then return "tier-b" end
if t:find("c") and t:find("tier") then return "tier-c" end
return "tier-def"
end
local FLAGS = {
india="🇮🇳", bangladesh="🇧🇩", pakistan="🇵🇰", nepal="🇳🇵",
srilanka="🇱🇰", myanmar="🇲🇲", indonesia="🇮🇩", thailand="🇹🇭",
malaysia="🇲🇾", philippines="🇵🇭", vietnam="🇻🇳", singapore="🇸🇬",
china="🇨🇳", japan="🇯🇵", korea="🇰🇷", usa="🇺🇸",
uk="🇬🇧", germany="🇩🇪", brazil="🇧🇷", australia="🇦🇺"
}
local function getFlag(country)
if not country or country == "" then return "" end
local key = country:lower():gsub("%s+",""):gsub("-","")
return (FLAGS[key] or "") .. " "
end
local function statusStyle(s)
s = (s or ""):lower()
if s == "active" then return "#4ade80","rgba(34,197,94,.2)", "rgba(34,197,94,.35)" end
if s == "inactive" then return "#fbbf24","rgba(251,191,36,.2)", "rgba(251,191,36,.35)" end
if s == "retired" then return "#94a3b8","rgba(148,163,184,.2)","rgba(148,163,184,.35)" end
if s == "banned" then return "#f87171","rgba(239,68,68,.2)", "rgba(239,68,68,.35)" end
return "#94a3b8","rgba(148,163,184,.2)","rgba(148,163,184,.35)"
end
-- ── Team info + logo helpers ──────────────────────────────────────
local teamInfoCache = {}
local function getTeamInfo(teamName)
if not teamName or teamName == "" then return nil end
if teamInfoCache[teamName] then return teamInfoCache[teamName] end
local rows = cargo.query("Teams",
"display_name,image,image_dark",
{ where = "name='" .. esc(teamName) .. "'", limit = 1 })
local result = (rows and #rows > 0) and rows[1] or nil
teamInfoCache[teamName] = result
return result
end
-- Outputs both light + dark logo, CSS switches via logo-lightmode/logo-darkmode
local function teamLogoWikitext(ti, size)
if not ti then return nil end
size = size or "26px"
local light = (ti.image and ti.image ~= "") and ti.image or nil
local dark = (ti.image_dark and ti.image_dark ~= "") and ti.image_dark or light
if not light then return nil end
return "[[File:"..light.."|"..size.."|link=|class=pd-team-logo-img logo-lightmode]]"
.. "[[File:"..dark .."|"..size.."|link=|class=pd-team-logo-img logo-darkmode]]"
end
-- Place badge matching achievements table style
local function renderPlaceBadge(td, placeNum)
local badgeClass = "place-def"
local placeText = tostring(placeNum) .. "th"
if placeNum == 1 then badgeClass = "place-1"; placeText = "1st"
elseif placeNum == 2 then badgeClass = "place-2"; placeText = "2nd"
elseif placeNum == 3 then badgeClass = "place-3"; placeText = "3rd"
end
td:tag('span'):addClass('place-badge ' .. badgeClass):wikitext(placeText)
end
-- ── Main ─────────────────────────────────────────────────────────
function p.main(frame)
local pageName = mw.title.getCurrentTitle().text
local sqlPage = esc(pageName)
-- ── 1. Player row ─────────────────────────────────────────────
local pRows = cargo.query("Players",
"id,real_name,image,current_team,nationality,status,game,"
.. "birth_date,role,instagram,youtube,twitter,discord,facebook",
{ where = "_pageName='" .. sqlPage .. "'", limit = 1 })
local pl = (pRows and #pRows > 0) and pRows[1] or {}
local displayName = pl.id or mw.title.getCurrentTitle().subpageText
local sqlName = esc(displayName)
-- ── 2. Krafton rank ───────────────────────────────────────────
local rankVal = nil
local rRows = cargo.query("Krafton_Rankings", "rank", {
where = string.format(
"(name='%s' OR name='%s') AND type='Player'", sqlPage, sqlName),
limit = 1 })
if rRows and #rRows > 0 then rankVal = "#" .. rRows[1].rank end
-- ── 3. All tournaments player appeared in ─────────────────────
local participation = cargo.query("Tournament_Teams",
"tournament,team,p1,p2,p3,p4,p5,p6",
{ where = string.format(
"p1='%s' OR p1='%s' OR p2='%s' OR p2='%s' OR "
.."p3='%s' OR p3='%s' OR p4='%s' OR p4='%s' OR "
.."p5='%s' OR p5='%s' OR p6='%s' OR p6='%s'",
sqlPage,sqlName, sqlPage,sqlName, sqlPage,sqlName,
sqlPage,sqlName, sqlPage,sqlName, sqlPage,sqlName),
limit = 500 })
participation = participation or {}
-- Tournament -> team lookup (for individual awards team column)
local tournTeamMap = {}
for _, r in ipairs(participation) do
if r.tournament and r.team and not tournTeamMap[r.tournament] then
tournTeamMap[r.tournament] = r.team
end
end
-- ── 4. Teammates ──────────────────────────────────────────────
-- Count how many tournaments each co-player appeared in with this player
-- Skip coach/analyst — only p1-p6. Skip the player himself.
local teammateCount = {}
local slots = {"p1","p2","p3","p4","p5","p6"}
for _, r in ipairs(participation) do
for _, slot in ipairs(slots) do
local name = r[slot]
if name and name ~= "" and name ~= pageName and name ~= displayName then
teammateCount[name] = (teammateCount[name] or 0) + 1
end
end
end
-- Sort teammates by frequency desc, take top 10
local teammates = {}
for name, count in pairs(teammateCount) do
table.insert(teammates, { name = name, count = count })
end
table.sort(teammates, function(a, b) return a.count > b.count end)
if #teammates > 10 then
local trimmed = {}
for i = 1, 10 do trimmed[i] = teammates[i] end
teammates = trimmed
end
-- ── 5. Earnings ───────────────────────────────────────────────
local indEarnings, teamEarnings = 0, 0
local eRows = cargo.query("PrizeMoney", "SUM(prize)=total", {
where = string.format(
"(player='%s' OR player='%s') AND player != ''", sqlPage, sqlName) })
if eRows and #eRows > 0 then
indEarnings = tonumber(eRows[1].total) or 0
end
if #participation > 0 then
local conds = {}
for _, r in ipairs(participation) do
if r.tournament and r.team then
table.insert(conds, string.format(
"(tournament='%s' AND team='%s')",
esc(r.tournament), esc(r.team)))
end
end
if #conds > 0 then
local tPrizes = cargo.query("PrizeMoney", "SUM(prize)=team_total", {
where = "(" .. table.concat(conds, " OR ") .. ")"
.. " AND (player='' OR player IS NULL)" })
if tPrizes and #tPrizes > 0 then
teamEarnings = tonumber(tPrizes[1].team_total) or 0
end
end
end
local totalEarnings = indEarnings + teamEarnings
-- ── 6. Team history ───────────────────────────────────────────
local teamHist = cargo.query("Player_Former_Teams",
"team,join_date,leave_date",
{ where = "player_id='" .. sqlPage .. "'",
orderBy = "join_date DESC", limit = 20 })
if not teamHist or #teamHist == 0 then
teamHist = cargo.query("Player_Former_Teams",
"team,join_date,leave_date",
{ where = "player_id='" .. sqlName .. "'",
orderBy = "join_date DESC", limit = 20 })
end
teamHist = teamHist or {}
-- ── 7a. Individual awards ─────────────────────────────────────
local indAwards = cargo.query("PrizeMoney",
"tournament,award,prize",
{ where = string.format(
"(player='%s' OR player='%s') AND player != ''",
sqlPage, sqlName),
orderBy = "tournament DESC", limit = 100 })
indAwards = indAwards or {}
-- ── 7b. Team tournament results ───────────────────────────────
local teamResults = {}
local seenKey = {}
if #participation > 0 then
local pairs_list = {}
for _, r in ipairs(participation) do
if r.tournament and r.team then
local k = r.tournament .. "|" .. r.team
if not seenKey[k] then
seenKey[k] = true
table.insert(pairs_list, { tournament = r.tournament, team = r.team })
end
end
end
if #pairs_list > 0 then
local ttConds = {}
for _, r in ipairs(pairs_list) do
table.insert(ttConds, string.format(
"(tournament='%s' AND team='%s')",
esc(r.tournament), esc(r.team)))
end
local whereStr = table.concat(ttConds, " OR ")
local placeIndex = {}
local placeRows = cargo.query("Tournament_Results",
"tournament,team,place",
{ where = whereStr, limit = 500 })
if placeRows then
for _, row in ipairs(placeRows) do
placeIndex[row.tournament .. "|" .. row.team] = tonumber(row.place)
end
end
local prizeIndex = {}
local prizeRows = cargo.query("PrizeMoney",
"tournament,team,prize,placement,award",
{ where = "(" .. whereStr .. ")"
.. " AND (player='' OR player IS NULL)",
limit = 500 })
if prizeRows then
for _, row in ipairs(prizeRows) do
local k = row.tournament .. "|" .. row.team
local newPrize = tonumber(row.prize) or 0
local existing = prizeIndex[k]
if not existing or newPrize > (existing.prize or 0) then
prizeIndex[k] = {
prize = newPrize,
placement = row.placement or "",
award = row.award or "",
}
end
end
end
for _, r in ipairs(pairs_list) do
local k = r.tournament .. "|" .. r.team
local pr = prizeIndex[k]
local pl_num = placeIndex[k]
if not pl_num and pr and pr.placement ~= "" then
local n = pr.placement:match("(%d+)")
if n then pl_num = tonumber(n) end
end
table.insert(teamResults, {
tournament = r.tournament,
team = r.team,
place = pl_num,
award = (pr and pr.award ~= "" and pr.award) or nil,
prize = (pr and pr.prize > 0 and pr.prize) or 0,
})
end
end
end
table.sort(teamResults, function(a, b)
if a.place and b.place then return a.place < b.place end
if a.place then return true end
if b.place then return false end
return (a.tournament or "") > (b.tournament or "")
end)
-- ── 8. Tournament metadata ────────────────────────────────────
local allTournNames = {}
local tournSeen = {}
local function addTourn(name)
if name and name ~= "" and not tournSeen[name] then
tournSeen[name] = true
table.insert(allTournNames, "'" .. esc(name) .. "'")
end
end
for _, a in ipairs(indAwards) do addTourn(a.tournament) end
for _, t in ipairs(teamResults) do addTourn(t.tournament) end
local tournMeta = {}
if #allTournNames > 0 then
local metaRows = cargo.query("Tournaments", "name,tier,end_date", {
where = "name IN (" .. table.concat(allTournNames, ",") .. ")",
limit = 200 })
if metaRows then
for _, row in ipairs(metaRows) do
tournMeta[row.name] = { tier = row.tier, date = row.end_date }
end
end
end
-- ── BUILD HTML ────────────────────────────────────────────────
local sc, sbg, sborder = statusStyle(pl.status)
local initials = displayName:sub(1, 2):upper()
local root = html.create('div'):addClass('pd-dashboard')
-- ════ HERO ════
-- Layout: [photo] [name + realname + tags] [earnings — hidden on mobile]
-- Krafton rank is NO LONGER here — moved to left info panel
local hero = root:tag('div'):addClass('pd-hero')
-- Photo
local photo = hero:tag('div'):addClass('pd-hero-photo')
if pl.image and pl.image ~= "" then
photo:wikitext("[[File:" .. pl.image .. "|120px|link=|class=pd-hero-img]]")
else
photo:wikitext(initials)
end
-- Name + tags (no rank chip here anymore)
local heroInfo = hero:tag('div'):addClass('pd-hero-info')
heroInfo:tag('div'):addClass('pd-hero-name'):wikitext(displayName)
if pl.real_name and pl.real_name ~= "" then
heroInfo:tag('div'):addClass('pd-hero-realname'):wikitext(pl.real_name)
end
local tags = heroInfo:tag('div'):addClass('pd-hero-tags')
if pl.status and pl.status ~= "" then
tags:tag('span'):addClass('pd-tag')
:css('color', sc):css('background', sbg)
:css('border', '1px solid ' .. sborder)
:wikitext('● ' .. pl.status)
end
if pl.role and pl.role ~= "" then
tags:tag('span'):addClass('pd-tag pd-tag-role'):wikitext(pl.role)
end
if pl.game and pl.game ~= "" then
tags:tag('span'):addClass('pd-tag pd-tag-game'):wikitext(pl.game)
end
-- Earnings: right side on desktop, separate row on mobile via CSS
-- pd-earnings-total wraps label+number so flex can split total vs breakdown
if totalEarnings > 0 then
local er = hero:tag('div'):addClass('pd-hero-right')
local et = er:tag('div'):addClass('pd-earnings-total')
et:tag('div'):addClass('pd-earnings-label'):wikitext('Approx. Earnings')
et:tag('div'):addClass('pd-earnings-val'):wikitext(fmtCurrency(totalEarnings))
local bd = er:tag('div'):addClass('pd-earnings-breakdown')
bd:tag('div')
:tag('div'):addClass('pd-earn-sub-label'):wikitext('Team'):done()
:tag('div'):addClass('pd-earn-sub-val'):wikitext(fmtCurrency(teamEarnings))
bd:tag('div')
:tag('div'):addClass('pd-earn-sub-label'):wikitext('Individual'):done()
:tag('div'):addClass('pd-earn-sub-val'):wikitext(fmtCurrency(indEarnings))
end
-- ════ BODY ════
local body = root:tag('div'):addClass('pd-body')
local left = body:tag('div'):addClass('pd-left')
left:tag('div'):addClass('pd-section-title'):wikitext('Player Info')
local function infoRow(label, val)
if not val or val == "" then return end
left:tag('div'):addClass('pd-info-row')
:tag('span'):addClass('pd-info-label'):wikitext(label):done()
:tag('span'):addClass('pd-info-val'):wikitext(val)
end
infoRow('Real Name', pl.real_name)
if pl.birth_date and pl.birth_date ~= "" then
infoRow('Born', lang:formatDate("d F Y", pl.birth_date))
end
if pl.nationality and pl.nationality ~= "" then
infoRow('Nationality', getFlag(pl.nationality) .. pl.nationality)
end
infoRow('Role', pl.role)
infoRow('Game', pl.game)
-- Current team: logo + display_name
if pl.current_team and pl.current_team ~= "" then
local ti = getTeamInfo(pl.current_team)
local teamDisplay = (ti and ti.display_name and ti.display_name ~= "")
and ti.display_name or pl.current_team
local logoHtml = teamLogoWikitext(ti, "22px")
local row = left:tag('div'):addClass('pd-info-row')
row:tag('span'):addClass('pd-info-label'):wikitext('Current Team')
local wrap = row:tag('span'):addClass('pd-info-val')
:tag('span'):addClass('pd-current-team')
if logoHtml then wrap:wikitext(logoHtml) end
wrap:tag('span'):addClass('pd-current-team-name')
:wikitext("[[" .. pl.current_team .. "|" .. teamDisplay .. "]]")
end
-- Krafton rank now sits naturally in the info panel
if rankVal then infoRow('Krafton Rank', rankVal) end
-- Socials
local socials = {
{ k='instagram', file='Icon_instagram.png', base='https://instagram.com/' },
{ k='twitter', file='Icon_twitter.png', base='https://twitter.com/' },
{ k='youtube', file='Icon_youtube.png', base='https://youtube.com/' },
{ k='discord', file='Icon_discord.png', base='https://discord.gg/' },
{ k='facebook', file='Icon_facebook.png', base='https://facebook.com/' },
}
local socDiv = left:tag('div'):addClass('pd-socials')
for _, s in ipairs(socials) do
local v = pl[s.k]
if v and v ~= "" then
local url = v:match("^https?://") and v or (s.base .. v)
socDiv:wikitext("[[File:" .. s.file
.. "|32px|link=" .. url .. "|class=social-img]]")
end
end
-- Team history
if #teamHist > 0 then
left:tag('div'):addClass('pd-section-title pd-section-gap')
:wikitext('Team History')
for _, h in ipairs(teamHist) do
local ti = getTeamInfo(h.team or "")
local logoHtml = teamLogoWikitext(ti, "24px")
local tr = left:tag('div'):addClass('pd-team-row')
if logoHtml then
tr:wikitext(logoHtml)
else
tr:tag('div'):addClass('pd-team-logo-fb')
:wikitext((h.team or "?"):sub(1, 3):upper())
end
tr:tag('div'):addClass('pd-team-name')
:wikitext("[[" .. (h.team or "") .. "]]")
tr:tag('div'):addClass('pd-team-dates')
:wikitext(fmtDate(h.join_date) .. " – "
.. (h.leave_date and h.leave_date ~= ""
and fmtDate(h.leave_date) or "Now"))
end
end
-- ── Teammates section ─────────────────────────────────────────
-- Players who shared the most tournaments with this player
-- Source: Tournament_Teams p1-p6, coach/analyst excluded
if #teammates > 0 then
left:tag('div'):addClass('pd-section-title pd-section-gap')
:wikitext('Played With')
for _, tm in ipairs(teammates) do
local tr = left:tag('div'):addClass('pd-team-row')
tr:tag('div'):addClass('pd-teammate-name')
:wikitext("[[" .. tm.name .. "]]")
tr:tag('div'):addClass('pd-teammate-count')
:wikitext(tostring(tm.count)
.. (tm.count == 1 and " tournament" or " tournaments"))
end
end
-- ════ RIGHT PANEL ════
local right = body:tag('div'):addClass('pd-right')
local function renderTierCell(td, tier)
if tier and tier ~= "" then
td:addClass('ac-tier')
:tag('span'):addClass('tier-badge ' .. getTierClass(tier)):wikitext(tier)
else
td:wikitext("—")
end
end
local function renderDateCell(td, date)
td:addClass('ac-date'):wikitext(date and date ~= "" and date or "—")
end
-- Individual Awards
if #indAwards > 0 then
local sec = right:tag('div'):addClass('pd-prize-section')
sec:tag('div'):addClass('pd-table-title'):wikitext('🏆 Individual Awards')
local tbl = sec:tag('table'):addClass('pd-tourn-table')
local hdr = tbl:tag('tr')
hdr:tag('th'):wikitext('Date')
hdr:tag('th'):wikitext('Tier')
hdr:tag('th'):wikitext('Tournament')
hdr:tag('th'):wikitext('Team')
hdr:tag('th'):wikitext('Award')
hdr:tag('th'):css('text-align','right'):wikitext('Prize')
for _, a in ipairs(indAwards) do
local meta = tournMeta[a.tournament or ""] or {}
local tr = tbl:tag('tr')
renderDateCell(tr:tag('td'), meta.date)
renderTierCell(tr:tag('td'), meta.tier)
tr:tag('td'):css('font-weight','600')
:wikitext("[[" .. (a.tournament or "") .. "]]")
local playerTeam = tournTeamMap[a.tournament or ""]
tr:tag('td'):wikitext(playerTeam and ("[[" .. playerTeam .. "]]") or "—")
tr:tag('td'):tag('span'):addClass('pd-award-badge')
:wikitext(a.award and a.award ~= "" and a.award or "Award")
tr:tag('td'):addClass('ac-prize')
:wikitext(tonumber(a.prize) and tonumber(a.prize) > 0
and fmtCurrency(tonumber(a.prize)) or "—")
end
end
-- Team Tournament Results
local sec2 = right:tag('div'):addClass('pd-prize-section')
sec2:tag('div'):addClass('pd-table-title'):wikitext('🎖 Team Tournament Results')
if #teamResults > 0 then
local tbl2 = sec2:tag('table'):addClass('pd-tourn-table')
local hdr2 = tbl2:tag('tr')
hdr2:tag('th'):wikitext('Date')
hdr2:tag('th'):wikitext('Tier')
hdr2:tag('th'):wikitext('Tournament')
hdr2:tag('th'):wikitext('Team')
hdr2:tag('th'):addClass('ac-place'):wikitext('Place/Award')
hdr2:tag('th'):css('text-align','right'):wikitext('Prize')
for _, t in ipairs(teamResults) do
local meta = tournMeta[t.tournament or ""] or {}
local tr2 = tbl2:tag('tr')
renderDateCell(tr2:tag('td'), meta.date)
renderTierCell(tr2:tag('td'), meta.tier)
tr2:tag('td'):css('font-weight','600')
:wikitext("[[" .. t.tournament .. "]]")
tr2:tag('td'):wikitext("[[" .. t.team .. "]]")
local pTd = tr2:tag('td'):addClass('ac-place')
if t.place then
renderPlaceBadge(pTd, t.place)
elseif t.award then
pTd:tag('span'):addClass('pd-award-badge'):wikitext(t.award)
else
pTd:wikitext("—")
end
tr2:tag('td'):addClass('ac-prize')
:wikitext(t.prize > 0 and fmtCurrency(t.prize) or "—")
end
else
sec2:tag('div'):addClass('pd-empty')
:wikitext('No tournament data found.')
end
right:tag('div'):addClass('pd-stats-placeholder')
:tag('b'):wikitext('📊 Performance Stats'):done()
:wikitext('Coming soon — per-match stats will appear here.')
return tostring(root)
end
return p