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