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

Documentation for this module may be created at Module:Bracket/doc

-- ================================================================
-- Module:Bracket v3.0 (Smart Nodes, Game Links, Info Popups)
-- ================================================================
local p = {}
local html = mw.html
local cargo = mw.ext.cargo

local function esc(s) return s and s:gsub("\\", "\\\\"):gsub("'", "\\'") or "" end
local function clean(s)
    if not s then return nil end
    local c = s:gsub("[\r\n]",""):gsub("^%s*(.-)%s*$","%1")
    return c ~= "" and c or nil
end

-- Smarter Date Formatter (Handles YYYY-MM-DD and YYYY-MM-DD HH:MM)
local function formatDate(d)
    if not d or d == "" then return "" end
    local year, month, day, hour, min = string.match(d, "(%d%d%d%d)%-(%d%d)%-(%d%d)[T%s]*(%d*)%:*(%d*)")
    if year then
        local months = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"}
        local dateStr = tonumber(day) .. "-" .. months[tonumber(month)]
        if hour and hour ~= "" then
            dateStr = dateStr .. " " .. hour .. ":" .. (min ~= "" and min or "00")
        end
        return dateStr
    end
    return d 
end

local teamLogoCache = {}
local function getTeamLogo(teamName)
    if not teamName or teamName == "" or teamName == "TBD" then return "" end
    if teamLogoCache[teamName] then return teamLogoCache[teamName] end
    
    local rows = cargo.query("Teams","image,image_dark",{where="name='"..esc(teamName).."'", limit=1})
    local t = (rows and #rows > 0) and rows[1] or false
    if not t then 
        teamLogoCache[teamName] = ""
        return "" 
    end
    
    local light = t.image or ""
    local dark = (t.image_dark and t.image_dark ~= "") and t.image_dark or light
    if light == "" then 
        teamLogoCache[teamName] = ""
        return "" 
    end
    
    local htmlStr = "[[File:"..light.."|18px|link=|class=logo-lightmode bk-team-logo]]"
        .. "[[File:"..dark .."|18px|link=|class=logo-darkmode bk-team-logo]]"
    teamLogoCache[teamName] = htmlStr
    return htmlStr
end

local function fetchMatch(event, matchID)
    if not event or event == "" then return {} end
    local rows = cargo.query("BracketMatch",
        "team1,score1,team2,score2,winner,bo,match_date,casters,vod,notes,short1,short2",
        { where = "event='"..esc(event).."' AND match_id='"..esc(matchID).."'", limit = 1 })
    if rows and #rows > 0 then return rows[1] end
    return {}
end

local function mergeMatchData(cargoData, args, prefix)
    local d = {}
    d.label   = clean(args[prefix.."label"])
    d.team1   = clean(args[prefix.."team1"])   or clean(cargoData.team1)   or "TBD"
    d.team2   = clean(args[prefix.."team2"])   or clean(cargoData.team2)   or "TBD"
    d.short1  = clean(args[prefix.."short1"])  or clean(cargoData.short1)  or d.team1
    d.short2  = clean(args[prefix.."short2"])  or clean(cargoData.short2)  or d.team2
    d.score1  = clean(args[prefix.."score1"])  or clean(cargoData.score1)  or ""
    d.score2  = clean(args[prefix.."score2"])  or clean(cargoData.score2)  or ""
    d.winner  = clean(args[prefix.."win"])     or clean(cargoData.winner)  or ""
    d.bo      = clean(args[prefix.."bo"])      or clean(cargoData.bo)      or ""
    d.date    = clean(args[prefix.."date"])    or clean(cargoData.match_date) or ""
    d.casters = clean(args[prefix.."casters"]) or clean(cargoData.casters) or ""
    d.vod     = clean(args[prefix.."vod"])     or clean(cargoData.vod)     or ""
    d.notes   = clean(args[prefix.."notes"])   or clean(cargoData.notes)   or ""
    return d
end

-- ── NEW MODERN MATCH CARD ──
local function renderMatchCard(d, matchID, defaultLabel, winTo, loseTo, target, isDrop, extraClass, game)
    local box = html.create('div'):addClass('bk-match')
        :attr('id', 'match-' .. matchID)
    
    if winTo and winTo ~= "" then box:attr('data-target-win', 'match-' .. winTo) end
    if loseTo and loseTo ~= "" then box:attr('data-target-lose', 'match-' .. loseTo) end
    if target and target ~= "" then box:attr('data-target', 'match-' .. target) end
    if isDrop then box:attr('data-drop', 'true') end
    if extraClass then box:addClass(extraClass) end

    local win1 = (d.winner == "1" or d.winner == d.team1) and d.team1 ~= "TBD"
    local win2 = (d.winner == "2" or d.winner == d.team2) and d.team2 ~= "TBD"

    -- Top Bar (Entire bar is now the clickable trigger)
    local top = box:tag('div'):addClass('bk-match-top')
       :attr('data-matchid', matchID) -- Ready for your future Match Page logic
       :attr('data-team1', d.team1):attr('data-team2', d.team2)
       :attr('data-score1', d.score1):attr('data-score2', d.score2)
       :attr('data-bo', d.bo):attr('data-date', d.date)
       :attr('data-casters', d.casters):attr('data-vod', d.vod):attr('data-notes', d.notes)
       :attr('title', 'Click for Match Details')
    
    -- The Label
    top:tag('span'):addClass('bk-match-label')
       :wikitext(d.label or defaultLabel or matchID)
    
    -- Meta Info (ONLY Date)
    local metaStr = formatDate(d.date)
    top:tag('span'):addClass('bk-match-meta'):wikitext(metaStr)

    -- Teams Wrapper
    local teamsWrap = box:tag('div'):addClass('bk-teams-wrap')

    local linkPrefix = ""
    if game and game ~= "" and game ~= "BGMI" then linkPrefix = game .. "/Teams/" end

    -- Team 1
    local row1 = teamsWrap:tag('div'):addClass('bk-team')
    if win1 then row1:addClass('bk-win') elseif win2 then row1:addClass('bk-lose') end
    row1:wikitext(getTeamLogo(d.team1))
    local t1Link = d.team1 ~= "TBD" and "[[" .. linkPrefix .. d.team1 .. "|" .. d.short1 .. "]]" or "TBD"
    row1:tag('span'):addClass('bk-team-name'):wikitext(t1Link)
    row1:tag('span'):addClass('bk-score'):wikitext(d.score1 ~= "" and d.score1 or "-")

    -- Team 2
    local row2 = teamsWrap:tag('div'):addClass('bk-team')
    if win2 then row2:addClass('bk-win') elseif win1 then row2:addClass('bk-lose') end
    row2:wikitext(getTeamLogo(d.team2))
    local t2Link = d.team2 ~= "TBD" and "[[" .. linkPrefix .. d.team2 .. "|" .. d.short2 .. "]]" or "TBD"
    row2:tag('span'):addClass('bk-team-name'):wikitext(t2Link)
    row2:tag('span'):addClass('bk-score'):wikitext(d.score2 ~= "" and d.score2 or "-")

    return box
end

-- ================================================================
-- FORMAT: SINGLE ELIMINATION
-- ================================================================
local function renderSingleElim(args, event, teamCount, game)
    local rounds = math.floor(math.log(teamCount) / math.log(2))
    local suffixNames = { "Grand Final", "Semifinals", "Quarterfinals", "Round of 16", "Round of 32" }
    
    local scroll = html.create('div'):addClass('bk-scroll')
    local wrapper = scroll:tag('div'):addClass('bk-wrapper')

    for r = 1, rounds do
        local col = wrapper:tag('div'):addClass('bk-col')
        local pos = rounds - r + 1
        col:tag('div'):addClass('bk-col-header'):wikitext(args["r"..r.."name"] or suffixNames[pos] or ("Round "..r))

        local matchCount = teamCount / (2^r)
        for m = 1, matchCount do
            local matchID = "R"..r.."M"..m
            local cd = fetchMatch(event, matchID)
            local d  = mergeMatchData(cd, args, matchID)
            
            local target = ""
            local isFinal = (r == rounds)
            if not isFinal then
                local nextMatchNum = math.ceil(m / 2)
                target = "R"..(r+1).."M"..nextMatchNum
            end

            local card = renderMatchCard(d, matchID, "Match "..m, nil, nil, target, false, isFinal and "bk-final-match" or "", game)
            col:node(card)
        end
    end
    return tostring(scroll)
end

-- ================================================================
-- FORMAT: CUSTOM MANUAL BRACKET
-- ================================================================
local function renderCustom(args, event, game)
    local scroll = html.create('div'):addClass('bk-scroll')
    local wrapper = scroll:tag('div'):addClass('bk-wrapper')
    
    local cols = tonumber(args.columns) or 2
    for c = 1, cols do
        local col = wrapper:tag('div'):addClass('bk-col')
        
        local colName = clean(args["col"..c.."name"])
        if colName and colName:lower() ~= "none" then
            col:tag('div'):addClass('bk-col-header'):wikitext(colName)
        end
        
        local matchesStr = args["col"..c.."_matches"]
        if matchesStr then
            for mID in string.gmatch(matchesStr, '([^,]+)') do
                mID = clean(mID)
                local cd = fetchMatch(event, mID)
                local d  = mergeMatchData(cd, args, mID)
                local winTo = args[mID.."_win_to"]
                local loseTo = args[mID.."_lose_to"]
                local target = args[mID.."_target"] 
                local isDrop = args[mID.."_drop"] == "true" 
                
                col:node(renderMatchCard(d, mID, args[mID.."_label"], winTo, loseTo, target, isDrop, "", game))
            end
        end
    end
    return tostring(scroll)
end

-- ================================================================
-- MAIN ENTRY
-- ================================================================
function p.main(frame)
    local args = (frame:getParent() and frame:getParent().args) or frame.args
    local format = (clean(args.format) or "single"):lower()
    local event = clean(args.event) or ""
    local game = clean(args.game) or "" -- Pass the game prefix (e.g. "Honor of Kings")
    local teamCount = tonumber(args.teams) or 8

    local root = html.create('div'):addClass('bk-root')
    local bracketHTML

    if format == "single" then
        bracketHTML = renderSingleElim(args, event, teamCount, game)
    elseif format == "custom" then
        bracketHTML = renderCustom(args, event, game)
    else
        bracketHTML = "<div style='color:red; padding:20px'>Format '"..format.."' is currently being upgraded to v3.0. Use 'single' or 'custom' for now.</div>"
    end

    root:wikitext(bracketHTML)

    -- MODAL POPUP
    local overlay = root:tag('div'):addClass('bk-modal-overlay')
    local modal = overlay:tag('div'):addClass('bk-modal')
    modal:tag('span'):addClass('bk-modal-close'):attr('role','button'):wikitext('&#10005;')
    local mheader = modal:tag('div'):addClass('bk-modal-header')
    local mteams = mheader:tag('div'):addClass('bk-modal-teams')
    mteams:tag('span'):addClass('bk-modal-t1')
    mteams:tag('span'):addClass('bk-modal-vs'):wikitext('vs')
    mteams:tag('span'):addClass('bk-modal-t2')
    local mscore = mheader:tag('div'):addClass('bk-modal-score')
    mscore:tag('span'):addClass('bk-modal-s1')
    mscore:tag('span'):addClass('bk-modal-dash'):wikitext('&#8211;')
    mscore:tag('span'):addClass('bk-modal-s2')
    local mmeta = modal:tag('div'):addClass('bk-modal-meta')
    mmeta:tag('div'):addClass('bk-modal-row bk-modal-bo')
    mmeta:tag('div'):addClass('bk-modal-row bk-modal-date')
    mmeta:tag('div'):addClass('bk-modal-row bk-modal-casters')
    mmeta:tag('div'):addClass('bk-modal-row bk-modal-notes')
    mmeta:tag('div'):addClass('bk-modal-row bk-modal-vod')

    return tostring(root)
end

return p