Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:PlayerDashboard: Difference between revisions

From eSportsAmaze
No edit summary
No edit summary
 
(9 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:PlayerDashboard
-- Full-width player dashboard. Reads all data from existing Cargo tables.
-- No changes to any existing module or template needed.
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 s = tostring(math.floor(n))
     local num = math.floor(tonumber(n) or 0)
     local l3 = s:sub(-3)
     local s  = tostring(num)
     local rest = s:sub(1,-4)
    if #s <= 3 then return "₹ " .. s end
     local fmt = rest:reverse():gsub("(%d%d)","%1,"):reverse()
    local result    = s:sub(-3)
     if fmt ~= "" then l3 = "," .. l3 end
     local remaining = s:sub(1, -4)
     return "₹ " .. fmt .. l3
     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 statusColor(s)
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 "#22c55e", "rgba(34,197,94,.15)",  "rgba(34,197,94,.3)" end
     if s == "active"  then return "#4ade80","rgba(34,197,94,.2)",  "rgba(34,197,94,.35)"   end
     if s == "inactive" then return "#f59e0b", "rgba(245,158,11,.15)", "rgba(245,158,11,.3)" 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,.15)","rgba(148,163,184,.3)"end
     if s == "retired"  then return "#94a3b8","rgba(148,163,184,.2)","rgba(148,163,184,.35)" end
     if s == "banned"  then return "#ef4444", "rgba(239,68,68,.15)",   "rgba(239,68,68,.3)" end
     if s == "banned"  then return "#f87171","rgba(239,68,68,.2)", "rgba(239,68,68,.35)"   end
     return "#94a3b8", "rgba(148,163,184,.15)", "rgba(148,163,184,.3)"
     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   = mw.title.getCurrentTitle().text
     local pageName = mw.title.getCurrentTitle().text
     local sqlPage   = esc(pageName)
     local sqlPage = esc(pageName)


     -- ── 1. Fetch player row from Players table ──────────────
     -- ── 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,"
        "instagram,youtube,twitter,discord,facebook",
        .. "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 = "Unranked"
     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(
            sqlPage, esc(displayName)), limit = 1 })
            "(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 pWhere = string.format("(player='%s' OR player='%s')", sqlPage, esc(displayName))
    local eRows = cargo.query("PrizeMoney", "SUM(prize)=total", { where = pWhere })
    if eRows and #eRows > 0 then indEarnings = tonumber(eRows[1].total) or 0 end


     local participation = cargo.query("Tournament_Teams", "tournament,team", {
     local eRows = cargo.query("PrizeMoney", "SUM(prize)=total", {
         where = string.format(
         where = string.format(
             "p1='%s' OR p1='%s' OR p2='%s' OR p2='%s' OR " ..
             "(player='%s' OR player='%s') AND player != ''", sqlPage, sqlName) })
            "p3='%s' OR p3='%s' OR p4='%s' OR p4='%s' OR " ..
    if eRows and #eRows > 0 then
            "p5='%s' OR p5='%s' OR p6='%s' OR p6='%s'",
        indEarnings = tonumber(eRows[1].total) or 0
            sqlPage,esc(displayName), sqlPage,esc(displayName),
    end
            sqlPage,esc(displayName), sqlPage,esc(displayName),
            sqlPage,esc(displayName), sqlPage,esc(displayName)),
        limit = 500 })


     if participation and #participation > 0 then
     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)" })
                    .. " 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


     -- ── 4. Team history ──────────────────────────────────────
     -- ── 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=15 })
         { 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='"..esc(displayName).."'", orderBy="join_date DESC", limit=15 })
             { where = "player_id='" .. sqlName .. "'",
              orderBy = "join_date DESC", limit = 20 })
     end
     end
    teamHist = teamHist or {}


     -- ── 5. Tournament history ─────────────────────────────────
     -- ── 7a. Individual awards ─────────────────────────────────────
     -- For each tournament the player appeared in, find their team's
     local indAwards = cargo.query("PrizeMoney",
    -- final placement from StageStandings and prize from PrizeMoney
        "tournament,award,prize",
    local tournHistory = {}
        { where = string.format(
     local seenTournStage = {}
            "(player='%s' OR player='%s') AND player != ''",
            sqlPage, sqlName),
          orderBy = "tournament DESC", limit = 100 })
     indAwards = indAwards or {}


     if participation and #participation > 0 then
    -- ── 7b. Team tournament results ───────────────────────────────
         local tournTeamConds = {}
    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(tournTeamConds, string.format(
                 local k = r.tournament .. "|" .. r.team
                    "(tournament='%s' AND team='%s')", esc(r.tournament), esc(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
        if #tournTeamConds > 0 then
            local standings = cargo.query("StageStandings",
                "tournament,stage,team,totalpts,wwcd,elimpts,result",
                { where = table.concat(tournTeamConds," OR "),
                  orderBy = "tournament DESC", limit = 200 })


             for _, row in ipairs(standings) do
        if #pairs_list > 0 then
                 local key = row.tournament .. "|" .. row.team
            local ttConds = {}
                -- Keep only the last stage per tournament per team (Finals > Semis > etc.)
             for _, r in ipairs(pairs_list) do
                if not seenTournStage[key] then
                 table.insert(ttConds, string.format(
                    seenTournStage[key] = true
                    "(tournament='%s' AND team='%s')",
                    -- Get rank within that stage
                    esc(r.tournament), esc(r.team)))
                    local rankRows = cargo.query("StageStandings",
            end
                        "COUNT(team)=cnt",
            local whereStr = table.concat(ttConds, " OR ")
                        { where = string.format(
 
                            "tournament='%s' AND stage='%s' AND totalpts >= %s",
            local placeIndex = {}
                            esc(row.tournament), esc(row.stage),
            local placeRows  = cargo.query("Tournament_Results",
                            tonumber(row.totalpts) or 0) })
                "tournament,team,place",
                    local place = (rankRows and #rankRows > 0)
                { where = whereStr, limit = 500 })
                        and (tonumber(rankRows[1].cnt) or 99) or 99
            if placeRows then
                for _, row in ipairs(placeRows) do
                    placeIndex[row.tournament .. "|" .. row.team] = tonumber(row.place)
                end
            end


                    -- Prize for this tournament + team
            local prizeIndex = {}
                    local prizeRows = cargo.query("PrizeMoney", "SUM(prize)=total", {
            local prizeRows = cargo.query("PrizeMoney",
                        where = string.format(
                "tournament,team,prize,placement,award",
                            "tournament='%s' AND team='%s' AND (player='' OR player IS NULL)",
                { where = "(" .. whereStr .. ")"
                            esc(row.tournament), esc(row.team)) })
                    .. " AND (player='' OR player IS NULL)",
                     local prize = (prizeRows and #prizeRows > 0)
                  limit = 500 })
                         and (tonumber(prizeRows[1].total) or 0) or 0
            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


                    table.insert(tournHistory, {
            for _, r in ipairs(pairs_list) do
                        tournament = row.tournament,
                local k      = r.tournament .. "|" .. r.team
                        stage      = row.stage,
                local pr    = prizeIndex[k]
                        team      = row.team,
                local pl_num = placeIndex[k]
                        place      = place,
                if not pl_num and pr and pr.placement ~= "" then
                        result    = row.result or "",
                    local n = pr.placement:match("(%d+)")
                        prize      = prize
                     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 ────────────────────────────────────────────
     -- ── BUILD HTML ────────────────────────────────────────────────
    local sc, sbg, sborder = statusColor(pl.status)
    local initials = displayName:sub(1,2):upper()


     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


    -- ── HERO ─────────────────────────────────────────────────
     local hero = root:tag('div'):addClass('pd-hero')
     local hero = root:tag('div'):addClass('pd-hero')


     local photoDiv = hero:tag('div'):addClass('pd-hero-photo')
    -- Photo
     local photo = hero:tag('div'):addClass('pd-hero-photo')
     if pl.image and pl.image ~= "" then
     if pl.image and pl.image ~= "" then
         photoDiv:wikitext('[[File:'..pl.image..'|120px|link=]]')
         photo:wikitext("[[File:" .. pl.image .. "|120px|link=|class=pd-hero-img]]")
     else
     else
         photoDiv:wikitext(initials)
         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
    if pl.nationality and pl.nationality ~= "" then
        tags:tag('span'):addClass('pd-tag pd-tag-nat'):wikitext(pl.nationality)
    end
    if rankVal ~= "Unranked" then
        heroInfo:tag('div'):addClass('pd-rank-chip')
            :tag('span'):wikitext('Krafton Rank'):done()
            :tag('b'):wikitext(rankVal)
     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 heroRight = hero:tag('div'):addClass('pd-hero-right')
         local er = hero:tag('div'):addClass('pd-hero-right')
         heroRight:tag('div'):addClass('pd-earnings-label'):wikitext('Approx. Earnings')
         local et = er:tag('div'):addClass('pd-earnings-total')
         heroRight:tag('div'):addClass('pd-earnings-val'):wikitext(fmtCurrency(totalEarnings))
        et:tag('div'):addClass('pd-earnings-label'):wikitext('Approx. Earnings')
         local bd = heroRight:tag('div'):addClass('pd-earnings-breakdown')
         et:tag('div'):addClass('pd-earnings-val'):wikitext(fmtCurrency(totalEarnings))
         bd:tag('div'):addClass('pd-earn-sub')
         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'):addClass('pd-earn-sub')
         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 ─────────────────────────────────────────────────
     -- ════ BODY ════
 
     local body = root:tag('div'):addClass('pd-body')
     local body = root:tag('div'):addClass('pd-body')
    -- Left panel
     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, link)
     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(label)
            :tag('span'):addClass('pd-info-label'):wikitext(label):done()
         local vspan = row:tag('span'):addClass('pd-info-val')
            :tag('span'):addClass('pd-info-val'):wikitext(val)
         if link then vspan:wikitext('[['..val..']]')
    end
        else vspan: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
     end


     infoRow('Real Name',  pl.real_name)
     -- Krafton rank now sits naturally in the info panel
    infoRow('Born',        pl.birth_date and lang:formatDate("d F Y", pl.birth_date))
     if rankVal then infoRow('Krafton Rank', rankVal) end
    infoRow('Nationality', pl.nationality)
    infoRow('Role',        pl.role)
    infoRow('Current Team',pl.current_team, true)
    infoRow('Game',        pl.game)
     infoRow('Krafton Rank',rankVal ~= "Unranked" and rankVal or nil)


     -- 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 socialDiv = left:tag('div'):addClass('pd-socials')
     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)
             socialDiv:wikitext('[[File:'..s.file..'|24px|link='..url..'|class=social-img]]')
             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-title-gap'):wikitext('Team History')
         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-team-logo')
             tr:tag('div'):addClass('pd-teammate-name')
                 :wikitext((h.team or "?"):sub(1,3):upper())
                 :wikitext("[[" .. tm.name .. "]]")
            tr:tag('div'):addClass('pd-team-name'):wikitext('[['.. (h.team or "") ..']]')
             tr:tag('div'):addClass('pd-teammate-count')
             tr:tag('div'):addClass('pd-team-dates')
                 :wikitext(tostring(tm.count)
                 :wikitext(fmtDate(h.join_date) .. ' – ' ..
                    .. (tm.count == 1 and " tournament" or " tournaments"))
                    (h.leave_date and h.leave_date ~= ""
                        and fmtDate(h.leave_date) or 'Now'))
         end
         end
     end
     end


     -- Right panel — tournament history
     -- ════ RIGHT PANEL ════
 
     local right = body:tag('div'):addClass('pd-right')
     local right = body:tag('div'):addClass('pd-right')
    right:tag('div'):addClass('pd-section-title'):wikitext('Tournament History')


     if #tournHistory > 0 then
    local function renderTierCell(td, tier)
         local tbl = right:tag('table'):addClass('pd-tourn-table')
        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 _, h in ipairs({'Tournament','Team','Stage','Place','Result','Prize'}) do
        hdr:tag('th'):wikitext('Date')
             hdr:tag('th'):wikitext(h)
        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
        for _, t in ipairs(tournHistory) do
    end
            local tr    = tbl:tag('tr')
 
            local place = t.place
    -- Team Tournament Results
            local placeClass = place == 1 and 'pd-place-chip place-1'
    local sec2 = right:tag('div'):addClass('pd-prize-section')
                            or place == 2 and 'pd-place-chip place-2'
    sec2:tag('div'):addClass('pd-table-title'):wikitext('🎖 Team Tournament Results')
                            or place == 3 and 'pd-place-chip place-3'
 
                            or 'pd-place-chip place-other'
    if #teamResults > 0 then
             local res = (t.result or ""):lower()
        local tbl2 = sec2:tag('table'):addClass('pd-tourn-table')
             local resBadge = res == 'q' and '<span class="pd-result-q">Qualified</span>'
        local hdr2 = tbl2:tag('tr')
                          or res == 'e' and '<span class="pd-result-e">Eliminated</span>'
        hdr2:tag('th'):wikitext('Date')
                          or ""
        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


             tr:tag('td'):wikitext('[['..t.tournament..']]')
             tr2:tag('td'):addClass('ac-prize')
            tr:tag('td'):wikitext('[['..t.team..']]')
            tr:tag('td'):wikitext(t.stage or "—")
            tr:tag('td'):tag('span'):addClass(placeClass):wikitext(tostring(place))
            tr:tag('td'):wikitext(resBadge)
            tr:tag('td'):addClass(t.prize > 0 and 'pd-prize-cell' or 'pd-prize-none')
                 :wikitext(t.prize > 0 and fmtCurrency(t.prize) or "—")
                 :wikitext(t.prize > 0 and fmtCurrency(t.prize) or "—")
         end
         end
     else
     else
         right:tag('div'):addClass('pd-empty')
         sec2:tag('div'):addClass('pd-empty')
             :wikitext('No tournament data found for this player.')
             :wikitext('No tournament data found.')
     end
     end


    -- Stats placeholder (ready for future)
     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 once Player_Stats data is available.')
         :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