Jump to content
Toggle menu
  • 1 articles
  • 7 files
  • 3 users
  • 102 edits
Shroudforge
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.
Module documentation
<details class="t-mbox t-mbox--notice navigation-not-searchable noexcerpt t-dependencies" open="no"><summary class="t-mbox__header">Technical detailswiki-tools · uses 2</summary>
Source
Synced with wiki-tools on GitHub
Used by
Unavailable: looking up the pages that use it failed on this render. Purge the page to retry.
Uses
Requires1
Styles1
</details>
{{#seo:

|type = website |description = Module:Countdown is a template page used on the Star Citizen Wiki. Templates are pages that are embedded (transcluded) into other pages to allow for the repetition of information. |site_name = Star Citizen Wiki |locale = en-gb }}

A live countdown display, split by responsibility: the module renders the markup, the data attributes, and a no-JS fallback date; a mainpage gadget computes the actual remaining time and animates the digit stack client-side.

Required by Module:Mainpage/Event and Module:Mainpage/Event/Legacy; not invoked from templates.

For module editors

API

p.render(props) returns <templatestyles> + markup, or raises a Lua error when ends is missing or either date is not readable by the content language's date formatter (mw.language:formatDate); both current callers wrap the call in pcall.

Field Type Description
starts ISO 8601 string Optional. Omit for something already under way; with it the gadget can also render a "starts in" tense.
ends ISO 8601 string Required.
label string Overrides the gadget's tense-derived label. Rarely wanted; the point is that the label follows the dates.
class string Extra class on the root.

p.main(frame) is the #invoke entry point (reads starts/ends/label/class via Module:Arguments).

Gotchas

  • The module deliberately does not compute remaining time: a value worked out at parse time freezes into the parser cache and can go arbitrarily stale. It emits the dates as data-gadget-mainpage-countdown-start/-end (+ -label) attributes for the gadget, and renders a no-JS fallback: the end date alone, or a starts – ends range with the start year dropped when both dates share one.
  • An unreadable date is a hard Lua error, not a silent empty render, because a typo here has to be loud.

Styles

class = 't-countdown--flat' is the module's own modifier for lying the clock down into a horizontal strip regardless of viewport (it also does this automatically below 639px). The gadget replaces the fallback with a live digit stack, adding t-countdown--live and, for colour, one of --upcoming or --running; --ended carries no colour rule at all, deliberately falling back to the root's --color-subtle.


require('strict')

--- @module Countdown
--- A live countdown: the hook a gadget animates, plus a fallback that is true
--- without JavaScript.
---
--- It deliberately does NOT compute the remaining time here. A value worked out
--- at parse time is frozen into the parser cache and can be arbitrarily stale —
--- and a countdown reading "2 days left" for an event that finished last week is
--- worse than no countdown at all. So the dates go out as data attributes, the
--- mainpage gadget does the arithmetic on load, and a reader without JavaScript
--- sees the date range, which stays true forever.
---
--- The module owns the markup and the styling; the gadget owns the numbers. That
--- is the same division Module:Entity's stat line uses, where the server renders
--- a plausible figure and the gadget corrects it — except here the honest
--- fallback is a different fact rather than a stale version of the same one.

local START_ATTR = 'data-gadget-mainpage-countdown-start'
local END_ATTR = 'data-gadget-mainpage-countdown-end'

local p = {}

--- @param iso string
--- @return string|nil formatted, or nil when the date cannot be read
local function formatDate(iso, format)
	if not iso or iso == '' then
		return nil
	end
	local ok, formatted = pcall(function()
		return mw.language.getContentLanguage():formatDate(format, iso)
	end)
	return ok and formatted or nil
end

--- Rejects a date the formatter cannot read. A typo has to be loud: silently
--- rendering nothing is the one outcome nobody notices, and this element's whole
--- job is to be the page's most conspicuous statement about time.
---
--- @param name string parameter name, for the message
--- @param iso string
local function assertReadable(name, iso)
	if not formatDate(iso, 'j F Y') then
		error('Countdown: ' .. name .. ' is not a readable date: ' .. tostring(iso))
	end
end

--- The no-JS reading: a date, or a range when both ends are known. The year is
--- dropped from the opening date when both fall in the same one, so the common
--- case reads "12 – 18 August 2026" rather than repeating itself.
---
--- @param starts string|nil
--- @param ends string
--- @return string
local function fallbackText(starts, ends)
	local endText = formatDate(ends, 'j F Y')
	if not endText then
		return ''
	end
	if not starts or starts == '' then
		return endText
	end

	local sameYear = formatDate(starts, 'Y') == formatDate(ends, 'Y')
	local startText = formatDate(starts, sameYear and 'j F' or 'j F Y')
	if not startText then
		return endText
	end
	return startText .. ' – ' .. endText
end

--- @class CountdownProps
--- @field starts? string  ISO 8601 start. Omit for something already under way;
---        with it the gadget can also render the "starts in" tense.
--- @field ends string     ISO 8601 end. Required.
--- @field label? string   Overrides the gadget's tense-derived label. Rarely
---        wanted — the whole point is that the label follows the dates.
--- @field class? string

--- @param props CountdownProps
--- @return string
function p.render(props)
	if not props.ends or props.ends == '' then
		error('Countdown: ends is required')
	end
	assertReadable('ends', props.ends)
	if props.starts and props.starts ~= '' then
		assertReadable('starts', props.starts)
	end

	local styles = mw.getCurrentFrame():extensionTag({
		name = 'templatestyles',
		args = { src = 'Module:Countdown/styles.css' },
	})

	local root = mw.html.create('div'):addClass('t-countdown')
	if props.class and props.class ~= '' then
		root:addClass(props.class)
	end
	root:attr(END_ATTR, props.ends)
	if props.starts and props.starts ~= '' then
		root:attr(START_ATTR, props.starts)
	end
	if props.label and props.label ~= '' then
		root:attr('data-gadget-mainpage-countdown-label', props.label)
	end

	-- The fallback is a real sentence-worth of fact, not an empty shell: if the
	-- gadget never runs, this is what the reader is left with.
	root:tag('div'):addClass('t-countdown__fallback'):wikitext(fallbackText(props.starts, props.ends))

	return styles .. tostring(root)
end

--- Wikitext entry point.
---
--- @param frame mw.frame
--- @return string
function p.main(frame)
	local args = require('Module:Arguments').getArgs(frame)
	return p.render({
		starts = args.starts,
		ends = args.ends,
		label = args.label,
		class = args.class,
	})
end

return p