Module:Fancy

From The Museum of Human Achievement

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

local p = {}

local function isValidUrl(target)
	return type(target) == "string" and target:match('^https?://[%w%-%._~:/%?#%[%]@!$&\'()*+,;=]+$') ~= nil
end

local function urlencode(str)
	if str == nil then
		return ""
	end
	str = tostring(str)
	str = str:gsub("\n", "\r\n")
	str = str:gsub("([^%w%-_%.%~])", function(c)
		return string.format("%%%02X", string.byte(c))
	end)
	return str
end

local function splitCSV(s, sep, trimSpaces)
    sep = sep or ","
    trimSpaces = trimSpaces ~= false   -- default to true

    local t = {}
    for token in s:gmatch("([^" .. sep .. "]+)") do
        if trimSpaces then
            token = token:match("^%s*(.-)%s*$")  -- strip spaces
        end
        table.insert(t, token)
    end
    return t
end

local function truncate(text, maxLen)
    if type(text) ~= "string" then return text end
    if #text <= maxLen then return text end
    -- Find the last space before or at maxLen
    local cut = nil
    for i = maxLen, 1, -1 do
        local c = text:sub(i,i)
        if c == " " or c == "\t" or c == "\n" then
            cut = i
            break
        end
    end
    -- If we didn’t find a space (very long single word), just hard‑cut
    if not cut then
        return text:sub(1, maxLen) .. " …"
    end
    -- Cut at the found position and trim any trailing spaces/newlines
    local trimmed = text:sub(1, cut):gsub("[ \t\n]+$","")
    return trimmed .. " …"
end

local function processDate(year, month, day, hour, minute)
	local monthNames = {
		January = 1, 
		February = 2, 
		March = 3, 
		April = 4, 
		May = 5, 
		June = 6,
		July = 7, 
		August = 8, 
		September = 9, 
		October = 10, 
		November = 11, 
		December = 12
	}

	local monthNumbers = {
		[1] = "January", 
		[2] = "February", 
		[3] = "March", 
		[4] = "April",
		[5] = "May", 
		[6] = "June", 
		[7] = "July", 
		[8] = "August",
		[9] = "September", 
		[10] = "October", 
		[11] = "November", 
		[12] = "December"
	}

	-- Normalize month
	local monthNum, monthName

	if type(month) == "string" then
		month = mw.text.trim(month)
		local m = month:match("^0?(%d+)$")
		if m then
			monthNum = tonumber(m)
			monthName = monthNumbers[monthNum]
		else
			monthName = month
			monthNum = monthNames[monthName]
		end
	elseif type(month) == "number" then
		monthNum = month
		monthName = monthNumbers[monthNum]
	end

	local result = { year = tonumber(year) }

	if monthNum and monthName then
		result.month = monthNum
		result.monthName = monthName
	end

	if day then
		result.day = tonumber(day)
	end
	
	if hour then
		result.hour = string.format("%02d", hour)
		if minute then
			result.minute = string.format("%02d", minute)
		end
	end
	
	return result
end

local function printDatePlain(dateTable)
	local year = dateTable.year or "0000"
	local month = dateTable.month or 1
	local day = dateTable.day or 1
	
	local hour = dateTable.hour or 0
	local minute = dateTable.minute or 0
	local second = dateTable.second or 0
	
	-- Ensure two-digit month and day, hour, minute and second
	local mm = string.format("%02d", month)
	local dd = string.format("%02d", day)
    local hh = string.format("%02d", hour)
    local ii = string.format("%02d", minute)
    local ss = string.format("%02d", second)
    
	return string.format("%d-%s-%s %s:%s:%s", year, mm, dd, hh, ii, ss)
end

local function printDateInterval(from, to)
	if not from then return nil end -- safety check

	if not to then
		-- Assume full single date: day, monthName, year
		if from.day and from.monthName and from.year then
			return string.format("%s %d, %d", from.monthName, from.day, from.year)
		elseif from.monthName and from.year then
			return string.format("%s %d", from.monthName, from.year)
		else
			return tostring(from.year)
		end
	end

	-- Same year
	if from.year == to.year then
		if not from.month or not to.month then
			return string.format("%d", from.year)
		end
		-- Same month
		if from.month == to.month then
			if from.day and to.day then
				if from.day == to.day then
					return string.format("%s %d, %d", from.monthName, from.day, from.year)
				else
					return string.format("%s %d–%d, %d", from.monthName, from.day, to.day, from.year)
				end
			elseif from.day then
				return string.format("%s %d, %d %d:%d-%d:%d", from.monthName, from.day, from.year, from.hour, from.minute, to.hour, to.minute)
			elseif to.day then
				return string.format("%s %d, %d", to.monthName, to.day, to.year)
			else
				return string.format("%s %d", from.monthName, from.year)
			end
		else
			-- Different months, same year
			if from.day and to.day then
				return string.format("%s %d – %s %d, %d", from.monthName, from.day, to.monthName, to.day, from.year)
			else
				return string.format("%s–%s %d", from.monthName, to.monthName, from.year)
			end
		end
	end

	-- Different years
	local function format(d)
		if d.day and d.monthName then
			return string.format("%s %d, %d", d.monthName, d.day, d.year)
		elseif d.monthName then
			return string.format("%s %d", d.monthName, d.year)
		else
			return tostring(d.year)
		end
	end

	return format(from) .. " – " .. format(to)
end

local function isoToTime(iso)
	local hour, tmin = iso:match("T(%d%d):(%d%d)")
	return hour .. ":" .. tmin
end
    
-- Helper function to convert date string to sortable numeric value (e.g. YYYYMMDD)
local function getOrderFromDate(date)
	if not date:match('^%d%d%d%d%-%d%d%-%d%d$') then
		return 99999999 -- fallback for invalid or missing date
	end
	local year, month, day = date:match('^(%d%d%d%d)%-(%d%d)%-(%d%d)$')
	return tonumber(year .. month .. day)
end

local function buttonlink(frame, target, text, class )
	-- Make URL
	if not isValidUrl(target) then
		target = frame:preprocess('{{fullurl:' .. target .. '}}')
	end
	-- Create button link
	local button = string.format('[%s <span class="%s">%s</span>]'
		, target
		, class
		, text
	)
	-- Assemble
	local html = mw.html.create()
	html:tag('div')
		:addClass('buttonlink moha-btn inverted mt-4 mb-0 text-center w-100')
		:wikitext(button)
	-- Output	
	return tostring(html)
end


function p.bannerPlain(frame)
	-- Get params
	local template = frame:getParent().args
	local heading = template[1] or ''
	local content = template[2] or ''
	-- Assemble
	local html = mw.html.create()
    local banner = html:tag('div'):addClass('banner-plain')
    if heading ~= '' then
		banner:tag('div'):addClass('h2 banner-heading'):wikitext(heading)
    end
	if content ~= '' then
		banner:tag('div'):addClass('banner-body'):wikitext(content)
    end
    -- Output
    return html
end

function p.bannerWithImage(frame)
	-- Get params
	local template = frame:getParent().args
	local heading = template[1] or ''
	local content = template[2] or ''
	local image   = template[3] or ''
	local class   = template[4] or ''
	-- Validate that image is a proper URL
	if not image:match('^https?://[%w%-%._~:/%?#%[%]@!$&\'()*+,;=]+$') then
		local imagename = frame:preprocess('{{PAGENAME:' .. image .. '}}')
		image = frame:preprocess('{{filepath:' .. imagename .. '}}')
	end
	-- Assemble
	local html = mw.html.create()
    local banner = html:tag('div'):addClass('banner-with-image d-flex flex-column flex-lg-row ' .. class)
    local info = banner:tag('div'):addClass('banner-info order-1 order-lg-0')
    if heading ~= '' then
		info:tag('div'):addClass('h2 banner-heading mx-lg-0 mb-4'):wikitext(heading)
    end
	if content ~= '' then
		info:tag('div'):addClass('banner-text mx-lg-0'):wikitext(content)
	end
	if image ~= '' then
		banner:tag('div'):addClass('banner-image order-0 order-lg-1'):attr('data-bg', image)
	end
    -- Output
    return html
end

function p.squareImage(frame)
	-- Get params
	local template = frame:getParent().args
	local image = template[1] or ''
	local size =  template[2] or '300px'
	local position = template[3] or 'right'
	-- Validate that image is a proper URL
	if not image:match('^https?://[%w%-%._~:/%?#%[%]@!$&\'()*+,;=]+$') then
		local imagename = frame:preprocess('{{PAGENAME:' .. image .. '}}')
		image = frame:preprocess('{{filepath:' .. imagename .. '|size}}')
	end
	-- Trim size
	size = size:match('^%s*(.-)%s*$')
	-- Assemble
	local html = mw.html.create()
	local wrapper = html:tag('div')
		:addClass('square-image float-md-' .. position)
		:css({
			width = size,
			height = size
		})
    local picture = wrapper:tag('div')
    	:addClass('w-100 h-100')
    	:attr('data-bg', image)
	-- Output
    return html
end

function p.bannerJoinUs(frame)
	-- Get params
	local this = frame.args
	local heading = this[1] or ''
	local content = this[2] or ''
	local newsletter = this['newsletter'] or 'false'
	local discord = this['discord'] or 'false'
	-- Assemble
	local html = mw.html.create()
    local banner = html:tag('div'):addClass('banner-plain')
    local wrapper = banner:tag('div'):addClass('d-flex flex-column flex-lg-row two-columns')
    local info = wrapper:tag('div'):addClass('d-flex flex-column banner-info text-left')
		:tag('div'):addClass('h2 banner-heading'):wikitext(heading):done()
    	:tag('div'):addClass('banner-body'):wikitext(content):done()
	local buttons = wrapper:tag('div'):addClass('d-flex flex-column banner-cta')
	-- Buttons
    if newsletter and newsletter ~= 'false' then 
    	local link = '[[Stay in Touch|<span class="cta">JOIN OUR NEWSLETTER</span><br><i class="icono-arrow1-left"></i>]]'
    	buttons:tag('div'):addClass('plainlinks newsletter'):wikitext(link)
    end
    if discord and discord ~= 'false' then 
    	local link = '[[Discord|<span class="cta">JOIN OUR DISCORD</span><br><i class="icono-arrow1-left"></i>]]'
    	buttons:tag('div'):addClass('plainlinks discord'):wikitext(link)
    end
    -- Output
    return html
end

function p.callButton(frame)
	-- Get params
	local template = frame:getParent().args
	local target   = template['target'] or 'Main Page'
	local label    = template['label'] or 'Call'
	local id       = template['id'] or ''
	local class    = template['class'] or ''
	-- Validate that target is a proper URL
    if not isValidUrl(target) then
		target = frame:preprocess('{{fullurl:' .. target .. '}}')
	end
	local link     = string.format('[%s %s]', target, label)
    -- Generate the button HTML
	local html = mw.html.create('div'):addClass('buttonlink'):attr('id', id)
		:tag('span'):addClass('moha-btn ' .. class):wikitext(link):done()
    -- Output
    return html
end


-- ########### CARD ###########

function p.card(frame)

	local params = frame:getParent().args
	local image  = params['image'] or 'Moha.wiki logo.png'
	local label  = params['label'] or 'MoHA'
	local content= params['content'] or ''
	local tagline= params['tagline'] or ''
	local target = params['target'] or 'Main Page'
	local buttontext = params['buttontext'] or 'LEARN MORE'
	local deadline = params['deadline'] or ''
	local order = getOrderFromDate(deadline)
	local class  = params['class'] or 'moha-card'
	-- Get image URL
	local imagepath = frame:preprocess('{{filepath:' .. image .. '}}')
	-- Sanitize target
	--if not isValidUrl(target) then
	--	target   = frame:preprocess('{{fullurl:' .. target .. '}}')
	--end
	html = mw.html.create()

	local card = html:tag('div')
    :addClass('card ' .. class)
    :css('order', order)
    
    local widget = frame:preprocess('{{#widget: a|href=/' .. target .. '}}')

	local cardimage = card:tag('div')
		:addClass('card-img-top')

		:tag('div')
			:addClass('image')
			:attr('data-bg', imagepath)
			:wikitext(widget)
			
			:tag('div')
				:tag('div')
					:addClass('overlap-label')
					:wikitext(label)
		:done()
		
	local cardbody = card:tag('div'):addClass('card-body')
		
	if tagline ~= '' then
		cardbody:tag('div')
			:addClass('card-tagline')
			:wikitext(tagline)
	end
	
	cardbody:tag('div')
		:addClass('card-text')
		:wikitext(content)
		:tag('div')
		:addClass('mt-auto')
		:wikitext(buttonlink(frame, target, buttontext, ''))
		:done()
		
	return html

end

function p.cardmore(frame)
	-- Get params	
	local params = frame:getParent().args
	local title  = params['title'] or ''
	local cta    = params['cta'] or ''
	local target = params['target'] or 'Main Page'
	-- Make URL
	if not isValidUrl(target) then
		target   = frame:preprocess('{{fullurl:' .. target .. '}}')
	end
	-- Build link
	local link   = string.format('[%s <span class="cta">%s</span><i class="icono-arrow1-left"></i>]'
		, target
		, cta
	)
	-- Assemble
	local html = mw.html.create()
	html:tag('div'):addClass('card morecards border-0'):css('order', 100000000)
		:tag('div'):addClass('h2'):wikitext(title):done()
		:tag('div'):addClass('continue'):wikitext(link)
	-- Output
	return html
end

local function getProgramImage(program, frame)
  local result = mw.smw.ask {
    '[[' .. program .. ']][[Modification date::+]]',
    'mainlabel=-',
    '?Program image#-='
  } or {}

  if #result == 0 then
    return nil
  end

  local imageName = result[1][1]
  if not imageName or imageName == '' then
    imageName = 'Moha.wiki logo.png'
  end
  
  local filePathTemplate = "{{filepath:{{PAGENAME:" .. imageName .. "}}}}"
  local out = frame:preprocess(filePathTemplate) or ''
  return out
end

local function getCurrentDate()
    return os.date("%Y-%m-%d")
end

function p.eventsNav(frame)
    local curDate = getCurrentDate()
    local formats = mw.smw.ask {
        '[[Category:Events]][[Is public::true]]' ..
        '[[Modification date::+]]',
        '[[Date Start::<' .. curDate .. ']]' .. 
        '[[Date End::>' .. curDate .. ']]' .. 
        ' OR ' ..
        '[[Category:Events]][[Is public::true]]' ..
        '[[Date Start::>' .. curDate .. ']]' .. 
        '[[Modification date::+]]',
        'mainlabel=-',
        'sort=Date Start',
        'order=asc',
        '?Event format=eFormat',
        'limit=150' -- @TEMP
    } or {}
    local uniqueFormats, seen = {}, {}
    if #formats > 0 then
        for _, row in ipairs(formats) do
            local val = row.eFormat
            if val then
                if type(val) == "table" then
                    -- multiple values
                    for _, v in ipairs(val) do
                        if v and not seen[v] then
                            uniqueFormats[#uniqueFormats + 1] = v
                            seen[v] = true
                        end
                    end
                else
                    -- single value
                    if not seen[val] then
                        uniqueFormats[#uniqueFormats + 1] = val
                        seen[val] = true
                    end
                end
            end
        end
    end
    -- Build HTML
    local html = mw.html.create()
    local nav = html:tag('div')
        :addClass("d-flex flex-column flex-lg-row justify-content-between events-monitor")

    local buttons = nav:tag('div')
        :addClass("d-flex justify-content-center w-100 flex-wrap px-2 py-5 event-filters")
        :css("gap", ".25rem")

    buttons:tag('span')
        :attr('id', 'sortToggle')
        :attr('title', 'Toggle sort order (by start date)')
        :addClass('badge rounded-0 d-flex fa-1x align-items-center text-nowrap nowrap')
        :tag('i')
        :addClass('fas fa-arrow-down'):done()
        :tag('span')
        :attr('id', 'sortOrderText')
        :addClass('d-none d-md-inline')
        :wikitext('Sorted ascending')
    if #formats > 0 then
        for _, eFormat in ipairs(uniqueFormats) do
            local pFormat = string.lower(eFormat)
            buttons:tag('span')
                :attr('id', pFormat)
                :addClass("badge badge-light rounded-0 d-flex fa-1x align-items-center text-nowrap nowrap toggle")
                :wikitext(pFormat)
        end
    end
    buttons:tag('span')
        :attr('id', 'ca-purge')
        :addClass("badge badge-light rounded-0 fa-1x d-flex align-items-center ca-purge")
        :css('gap', '.5rem')
        :wikitext(' RESET')
    -- The link to past events has moved to bottom
    -- buttons:tag('span')
    --	:wikitext('[[Past Events|<span id="past-events" class="badge rounded-0 fa-1x mb-1 px-3 d-flex align-items-center" style="gap:.5rem">Past Events <i class="fas fa-arrow-right"></i></span>]]')
    return html
end

function p.events(frame)
    
    local buttontext = "TICKETS"
    local eStartDateSort = '99999999'
    local curDate = frame:preprocess('{{#time: Y-m-d|now}}')
    
    local current = mw.smw.ask {
        '[[Category:Events]][[Is public::true]]' ..
        '[[Modification date::+]]',
        '[[Date Start::<' .. curDate .. ']]' .. 
        '[[Date End::>' .. curDate .. ']]' .. 
        ' OR ' ..
        '[[Category:Events]][[Is public::true]]' ..
        '[[Date Start::>' .. curDate .. ']]' .. 
        '[[Modification date::+]]',
        'mainlabel=-',
        '?#-=Event',
        '?Event image#-=dImage',
        '?Event image caption',
        '?Date Start#-F[Y-m-d]=dStart',
        '?Date End#-F[Y-m-d]=dEnd',
        '?Date Start#-F[g:i a]=tStart',
        '?Date End#-F[g:i a]=tEnd',
        '?Event format',
        '?Event location',
        '?Event admission type',
        '?Event admission price#=dPrice',
        '?Event admission price sliding low#=dLow',
        '?Event admission price sliding high#=dHigh',
        '?Associated Program',
        'sort=Date Start',
        'order=asc',
        'limit=150'
    } or {}
    
    -- Assemble
    local html = mw.html.create()
    local events = html:tag('div')
        :attr('id', 'card-wall-scrollable')
        :addClass("events card-wall")
    
    for _, event in ipairs(current) do
        local eItem              = event['Event']
        local eItemUrl           = frame:preprocess('{{fullurl:' .. eItem .. '}}') or ''
        local eLocation          = event['Event location']
        local eImage             = event['dImage'] or ''
        local eCaption           = event['Event image caption']
        -- Dates
        local dateStart          = event.dStart
        local dateEnd            = event.dEnd
        local eventStart         = processDate(
            tonumber(dateStart:sub(1, 4)), -- year
            tonumber(dateStart:sub(6, 7)), -- month
            tonumber(dateStart:sub(9, 10)) -- day
        )
        local eventEnd           = processDate(
            tonumber(dateEnd:sub(1, 4)), -- year
            tonumber(dateEnd:sub(6, 7)), -- month
            tonumber(dateEnd:sub(9, 10)) -- day
        )
        local curDate           = getOrderFromDate(os.date('%Y-%m-%d'))
        local printDate         = printDateInterval(eventStart, eventEnd)
        eStartDateSort  		= getOrderFromDate(dateStart)
        eEndDateSort  			= getOrderFromDate(dateEnd)
        local isCurrent = ''
        if curDate >= eStartDateSort and curDate <= eEndDateSort then
        	isCurrent = 'current'
        end
        -- Time
        local startTime = event.tStart
        local endTime = event.tEnd
        local printTime = ''
        if eStartDateSort == eEndDateSort and startTime ~= endTime then
        	printTime = startTime:gsub(":00", "") .. '–' .. endTime:gsub(":00", "")
        end
        -- Programs
    	local eProgram = event['Associated Program'] or ''
    	local pProgram, iProgram = '', ''
    	if type(eProgram) == 'table' then
    		pProgram = table.concat(eProgram, '|')
    		iProgram = eProgram[1]
    	else
    		pProgram = eProgram
    		iProgram = eProgram
    	end
        -- Formats
        local eFormat = event['Event format'] or ''
        local pFormat = ''
        local lFormatTable = {}
        if type(eFormat) == 'table' then
            for i, v in ipairs(eFormat) do
    			if type(v) == "string" then
        			eFormat[i] = v:gsub(" ", "_")
        			lFormat = frame:callParserFunction(
        						'#queryformlink',
        						'form=Event types',
        						'link text=' .. v,
        						'Event types[Event format]=' .. v,
        						'Event types[param]=Event format',
        						'Event types[value]=' .. v,
        						'_run=1'
           					)
        			table.insert(lFormatTable, lFormat)
    			end
            end
		    pFormat = table.concat(lFormatTable, ' | ')
            cFormat = string.lower(table.concat(eFormat, ','))
        else
            pFormat = frame:callParserFunction(
        						'#queryformlink',
        						'form=Event types',
        						'link text=' .. eFormat,
        						'Event types[Event format]=' .. eFormat,
        						'Event types[param]=Event format',
        						'Event types[value]=' .. eFormat,
        						'_run=1'
           					)
            cFormat = string.gsub(string.lower(eFormat), ' ', '_')
            -- cFormat = string.lower(eFormat)
        end
        -- Image
        local eImageUrl = ''
        if eImage ~= '' then 
            eImageUrlDecoded = mw.text.decode(frame:preprocess('{{PAGENAME:' .. eImage .. '}}'))
            eImageUrl = frame:preprocess('{{filepath:' .. eImageUrlDecoded .. '}}')
    	else 
    		if iProgram == nil or iProgram ~='' then 
    			eImageUrl = getProgramImage(iProgram, frame) or frame:preprocess('{{filepath:Moha.wiki logo.png}}')    
    		else
    			eImageUrl = frame:preprocess('{{filepath:Moha.wiki logo.png}}')
    		end
        end
        -- Admission
        local eaType = event['Event admission type'] or ''
        local eaPrice = event.dPrice or ''
		if eaPrice ~= '' then
			eaPrice = eaPrice:match("^(%d+)") or '0'
		end
		local eaPriceLow = event.dLow or ''
		if eaPriceLow ~= '' then
			eaPriceLow = eaPriceLow:match("^(%d+)") or '0'
		end
		local eaPriceHigh = event.dHigh or ''
		if eaPriceHigh ~= '' then
			eaPriceHigh = eaPriceHigh:match("^(%d+)") or '0'
		end
		local admission = ''
		if eaType == 'Free' then
		    admission = 'FREE'
		    buttontext = 'ATTEND'
		elseif eaType == 'Sliding Scale'  then
			
			if eaPriceHigh ~= eaPriceLow then
    			admission = string.format('%s %s%s-%s', 'Sliding Scale', '$', eaPriceLow, eaPriceHigh )
    		else
    			admission = string.format('%s%s', '$', eaPriceLow )
    		end
		else
    		if eaPrice ~= '0 USD' and eaPrice ~= '0' then
    			if eaType == 'Suggested Donation' then
        			admission = string.format('%s %s%s', 'Suggested Donation', '$', eaPrice)
        			buttontext = 'RSVP'
        		else
        			admission = string.format('%s%s', '$', eaPrice )
        			buttontext = 'TICKETS'
        		end
    		else
        		admission = 'FREE'
        		buttontext = 'ATTEND'
    		end
		end
        -- Link
        local target = eItem
        -- Widget
        local widget = frame:preprocess('{{#widget: a|href=' .. eItemUrl .. '}}')
        -- Card
        local card = events:tag('div')
        	:attr('data-event-format', cFormat)
        --	:attr('data-sort', eStartDateSort)
            :addClass('card moha-card ' .. isCurrent)
            :css('order', eStartDateSort)

        local cardimage = card:tag('div')
            :addClass('card-img-top')
            :tag('div')
            :addClass('image')
            :attr('data-bg', eImageUrl)
            :wikitext(widget)
        --				:tag('div')
        --					:tag('div')
        --						:addClass('overlap-label')
        --						:wikitext(oItem)
        --				:done()

        local cardbody = card:tag('div'):addClass('card-body')

        cardbody:tag('div')
            :addClass('card-title')
            :wikitext(string.format('[[%s]]', eItem))

        cardbody:tag('div')
            :addClass('mb-3 font-weight-bold')
            :wikitext(printDate .. ' ' .. printTime)
            :css('width', 'max-content')
            :css('max-width', '100%')

		local locadm = cardbody:tag('div')
			:addClass('mb-3 text-uppercase')
			
        	locadm:tag('span')
            	:addClass('card-tagline')
            	:wikitext(admission)
            	:css('width', 'max-content')
			
			if eLocation and eLocation ~= '' then
				local pLocation = ''
				if type(eLocation) == 'table' then
					pLocation = table.concat(eLocation, ' | ')
			        for i, v in ipairs(eLocation) do
    					if type(v) == "string" then
        					eLocation[i] = frame:callParserFunction(
        						'#queryformlink',
        						'form=Event types',
        						'link text=' .. v,
        						'Event types[Event location]=' .. v,
        						'Event types[param]=Event location',
        						'Event types[value]=' .. v,
        						'_run=1'
           					)
    					end
					end
				else
    				pLocation = frame:callParserFunction(
        						'#queryformlink',
        						'form=Event types',
        						'link text=' .. eLocation,
        						'Event types[Event location]=' .. eLocation,
        						'Event types[param]=Event location',
        						'Event types[value]=' .. eLocation,
        						'_run=1'
           					)
	    		end
				locadm:tag('span')
					:addClass('event-location')
            		:wikitext(string.format(' LOCATED AT: %s', pLocation))
			end
		
        if target ~= '' then
            cardbody:tag('div')
                :addClass('mt-auto')
                :tag('div'):addClass('event-formats text-left text-uppercase font-weight-bold'):wikitext(pFormat):done()
                :tag('div'):wikitext(buttonlink(frame, target, buttontext, ''))
        end
    end
    
    return html
end

function p.opportunitiesNav(frame)
	
	local formats = mw.smw.ask {
		'[[Category:Current Opportunities||Rolling Opportunities]][[Modification date::+]]',
    	'mainlabel=-',
    	'?Opportunity type=oType',
    	'limit=1000'
    }
    local uniqueFormats = {}
    -- Helper table that keeps track of what we’ve already stored
    local seen = {}
    for _, row in ipairs(formats) do
        local val = row['oType']
        if val and not seen[val] then
            uniqueFormats[#uniqueFormats + 1] = val
            seen[val] = true
        end
    end
    -- Assemble
    local html = mw.html.create()
    local nav = html:tag('div')
    	:addClass("d-flex flex-column flex-lg-row justify-content-between events-monitor")
    local buttons = nav:tag('div')
    	:addClass("d-flex justify-content-center w-100 flex-wrap px-2 py-5 event-filters")
    	:css("gap", ".25rem")
	for _, oType in ipairs(uniqueFormats) do
		local oType = string.lower(oType)
		buttons:tag('span')
			:attr('id', oType)
			:addClass("badge badge-light rounded-0 fa-1x mb-1 px-3 py-2 d-flex align-items-center toggle")
			:wikitext(oType .. ' ')
	end
  	buttons:tag('span')
  		:attr('id', 'ca-purge')
		:addClass("badge badge-light rounded-0 fa-1x mb-1 px-3 py-2 d-flex align-items-center ca-purge")
		:css('gap', '.5rem')
		:wikitext(' RESET')
	buttons:tag('span')
		:wikitext('[[:Category:Past Opportunities|<span id="past-events" class="badge rounded-0 fa-1x mb-1 px-3 d-flex align-items-center" style="gap:.5rem">Past Opportunities <i class="fas fa-arrow-right"></i></span>]]')
	return html
end

function p.opportunities(frame)
	local buttontext = "More Info"
	local args = frame:getParent().args
	local filterByType = args['type filter'] or '+'
	local current = mw.smw.ask {
		'[[Category:Current Opportunities]][[Modification date::+]][[Opportunity type::' .. filterByType .. ']]' ,
		'mainlabel=-',
		'?#-=Opportunity',
		'?Associated Program#-',
		'?Opportunity image#-=dImage',
		'?Opportunity type',
		'?Opportunity format',
		'?Opportunity start date#-F[Y-m-d]=dStart',
		'?Opportunity end date#-F[Y-m-d]=dEnd',
		'?Opportunity compensation type',
		'?Opportunity compensation',
		'?Opportunity link',
		'?Opportunity image',
		'?Opportunity link text',
		'?Opportunity second link',
		'?Opportunity second link text',
		'?Opportunity description',
		'?Opportunity short description',
		'sort=Opportunity end date',
		'order=asc',
		'limit=500'
	} or {}
	local rolling = mw.smw.ask {
		'[[Category:Rolling Opportunities]][[Modification date::+]][[Opportunity type::' .. filterByType .. ']]',
		'mainlabel=-',
		'?#-=Opportunity',
		'?Associated Program#-',
		'?Opportunity image#-=dImage',
		'?Opportunity type',
		'?Opportunity format',
		'?Opportunity start date#-F[Y-m-d]',
		'?Opportunity end date#-F[Y-m-d]',
		'?Opportunity compensation type',
		'?Opportunity compensation',
		'?Opportunity link',
		'?Opportunity image',
		'?Opportunity link text',
		'?Opportunity second link',
		'?Opportunity second link text',
		'?Opportunity description',
		'?Opportunity short description',
		'limit=500'
	} or {}
	
	-- Assemble
	local html = mw.html.create()
    local opportunities = html:tag('div')
    	:attr('id', 'card-wall-scrollable')
    	:addClass("opportunities card-wall")
    	
    	for _, opportunity in ipairs(current) do
    		local oItem = opportunity['Opportunity']
    		local oType = string.lower(opportunity['Opportunity type'])
    		local oDeadline = getOrderFromDate(opportunity.dEnd)
    		local oImage = opportunity.dImage or ''
    		-- Description
    		local oDesc = opportunity['Opportunity short description'] or ''
    		-- Dates
    		local dateStart = opportunity.dStart
    		local dateEnd   = opportunity.dEnd
    		local eventStart = processDate(
				tonumber(dateStart:sub(1,4)),   -- year
    			tonumber(dateStart:sub(6,7)),   -- month
    			tonumber(dateStart:sub(9,10))   -- day
   			)
			local eventEnd   = processDate(
				tonumber(dateEnd:sub(1,4)),   -- year
    			tonumber(dateEnd:sub(6,7)),   -- month
    			tonumber(dateEnd:sub(9,10))   -- day
			)
			local printDate = printDateInterval(eventStart, eventEnd)
    		-- Programs
    		local oProgram = opportunity['Associated Program']
    		local pProgram, iProgram = '', ''
    		if type(oProgram) == 'table' then
    			pProgram = table.concat(oProgram, '|')
    			iProgram = oProgram[1]
    		else
    			pProgram = oProgram
    			iProgram = oProgram
    		end
    		-- Formats
    		local oFormat = opportunity['Opportunity format'] or ''
    		local pFormat = ''
    		if type(oFormat) == 'table' then
    			pFormat = table.concat(oFormat, ' | ')
    		else
    			pFormat = oFormat
    		end
    		-- Image
    		local oImageUrl = ''
    		if oImage ~= '' then
    			oImageUrl = frame:preprocess('{{filepath:{{PAGENAME:' .. oImage .. '}}}}') 
    		else 
    			oImageUrl = getProgramImage(iProgram, frame)
    		end
    		-- Link
    		local oLink = opportunity['Opportunity link'] or ''
    		local target = oItem
    		-- Widget
        	local widget = frame:preprocess('{{#widget: a|href=/' .. urlencode(oItem) .. '}}')
    		-- Card
    		local card = opportunities:tag('div')
    			:addClass('card moha-card')
    			:attr('data-opportunity-type', oType)
    			:css('order', oDeadline)

    		local cardimage = card:tag('div')
				:addClass('card-img-top')
				:tag('div')
				:addClass('image')
				:attr('data-bg', oImageUrl)
				:wikitext(widget)
--				:tag('div')
--					:tag('div')
--						:addClass('overlap-label')
--						:wikitext(oItem)
--				:done()

			local cardbody = card:tag('div'):addClass('card-body')
			
				cardbody:tag('div')
					:addClass('card-title')
					:wikitext(string.format('[[%s]]', oItem))
					
				cardbody:tag('div')
					:addClass('card-tagline')
					:wikitext(printDate)
					:css('width', 'max-content')
				
				cardbody:tag('div')
					:addClass('card-text d-block pb-3')
					:wikitext('\n' .. truncate(oDesc, 300))

				if target ~= '' then
					cardbody:tag('div')
						:addClass('mt-auto')
						:tag('div'):addClass('text-left text-uppercase font-weight-bold'):wikitext(pFormat):done()
						:tag('div'):wikitext(buttonlink(frame, target, buttontext, ''))
				end
    	end
    	for _, opportunity in ipairs(rolling) do
    		local oItem = opportunity['Opportunity']
    		local oType = string.lower(opportunity['Opportunity type'])
    		local oDeadline = '99999999'
    		-- Description
    		local oDesc = opportunity['Opportunity short description'] or ''
    		-- Programs
    		local oProgram = opportunity['Associated Program']
    		local pProgram, iProgram = '', ''
    		if type(oProgram) == 'table' then
    			pProgram = table.concat(oProgram, '|')
    			iProgram = oProgram[1]
    		else
    			pProgram = oProgram
    			iProgram = oProgram
    		end
    		-- Formats
    		local oFormat = opportunity['Opportunity format'] or ''
    		local pFormat = ''
    		if type(oFormat) == 'table' then
    			pFormat = table.concat(oFormat, ' | ')
    		else
    			pFormat = oFormat
    		end
    		-- Image
    		local oImage = opportunity.dImage or ''
    		local oImageUrl = ''
    		if oImage ~= '' then
    			oImageUrl = frame:preprocess("{{filepath: {{PAGENAME:" .. oImage .. "}}}}") 
    		else 
    			oImageUrl = getProgramImage(iProgram, frame)
    		end
    		-- Tagline
    		local tagline = ('%s %s'):format('Rolling', oType)
			local upperTagline = string.upper(tagline)
			-- Link
    		local oLink = opportunity['Opportunity link'] or ''
    		local target = oItem
    		-- Widget
        	local widget = frame:preprocess('{{#widget: a|href=/' .. urlencode(oItem) .. '}}')
			-- Card
    		local card = opportunities:tag('div')
    			:addClass('card moha-card')
    			:attr('data-opportunity-type', oType)
    			:css('order', oDeadline)

    		local cardimage = card:tag('div')
				:addClass('card-img-top')
				:tag('div')
				:addClass('image')
				:attr('data-bg', oImageUrl)
				:wikitext(widget)

			local cardbody = card:tag('div'):addClass('card-body')
			 
				cardbody:tag('div')
					:addClass('card-title')
					:wikitext(string.format('[[%s]]', oItem))
					
				cardbody:tag('div')
					:addClass('card-tagline')
					:wikitext(upperTagline)
					:css('width', 'max-content')
					
				cardbody:tag('div')
					:addClass('card-text d-block pb-3')
					:wikitext('\n' .. truncate(oDesc, 300))
					
				if target ~= '' then
					cardbody:tag('div')
						:addClass('mt-auto')
						:tag('div'):addClass('text-left text-uppercase font-weight-bold'):wikitext(pFormat):done()
						:tag('div'):wikitext(buttonlink(frame, target, buttontext, ''))
				end
    	end
	return html
end

function p.test()
	local curdate = getCurrentDate()
	local formats = mw.smw.ask {
		'[[Category:Events]][[Is public::true]][[Date Start::+]][[Modification date::+]] ' ..
    	'OR ' ..
    	'[[Category:Events]][[Is public::true]][[Date Start::+]][[Date End::+]][[Modification date::+]]',
    	'mainlabel=-',
		'?Event format=eFormat',
		'limit=1000'
    } or {}
    if not formats then 
    	return
    end
    local uniqueFormats = {}
    -- Helper table that keeps track of what we’ve already stored
    local seen = {}
for _, row in ipairs(formats) do
    local val = row['eFormat']
    if val then
        if type(val) == "table" then
            -- iterate over multiple values
            for _, v in ipairs(val) do
                if v and not seen[v] then
                    uniqueFormats[#uniqueFormats + 1] = v
                    seen[v] = true
                end
            end
        else
            -- single value
            if not seen[val] then
                uniqueFormats[#uniqueFormats + 1] = val
                seen[val] = true
            end
        end
    end
end
	local html = mw.html.create()
    local nav = html:tag('div')
    	:addClass("d-flex flex-column flex-lg-row justify-content-between events-monitor")
    local buttons = nav:tag('div')
    	:addClass("d-flex justify-content-center w-100 flex-wrap px-2 py-5 event-filters")
    	:css("gap", ".25rem")
	for _, eFormat in ipairs(uniqueFormats) do
		local pFormat = string.lower(eFormat)
		buttons:tag('span')
			:attr('id', pFormat)
			:addClass("badge badge-light rounded-0 fa-1x mb-1 px-3 py-2 d-flex align-items-center toggle")
			:wikitext(pFormat)
	end
  	buttons:tag('span')
  		:attr('id', 'ca-purge')
		:addClass("badge badge-light rounded-0 fa-1x mb-1 px-3 py-2 d-flex align-items-center ca-purge")
		:css('gap', '.5rem')
		:wikitext(' RESET')
	buttons:tag('span')
		:wikitext('[[Past Events|<span id="past-events" class="badge rounded-0 fa-1x mb-1 px-3 d-flex align-items-center" style="gap:.5rem">Past Events <i class="fas fa-arrow-right"></i></span>]]')
	return html
end

function p.test()

end

return p