Candidate:Gadget-pickipedia-show.js
From PickiPedia: A knowledge base of bluegrass, old time psychedelic jams, and other public domain music
// |status=proposed -- bot-staged; remove this line when promoting.
/**
* Pickipedia — Show: page hero.
*
* For mainspace pages whose title starts with "Show:" (the legacy
* mainspace convention used pre-namespace), adds .pickipedia-show
* to body and absorbs Template:Show's infobox into a poster-style
* hero. If the page does not use Template:Show only the body class
* is added — the setlist typography in Common.css still applies.
*/
(function () {
'use strict';
if (typeof mw === 'undefined' || !mw.config) return;
var nsNumber = mw.config.get('wgNamespaceNumber');
var title = mw.config.get('wgTitle') || '';
var pageName = mw.config.get('wgPageName') || '';
var action = mw.config.get('wgAction') || 'view';
if (nsNumber !== 0) return;
if (action !== 'view') return;
if (title.indexOf('Show:') !== 0) return;
var body = document.body;
body.classList.add('pickipedia-show');
var content = document.querySelector('.mw-parser-output');
if (!content) return;
var infobox = content.querySelector('.show-infobox');
if (!infobox) return; // Bare show page — body class only.
// ---------- Pull fields out of the existing infobox ----------
// Template:Show emits rows of <div><strong>Label:</strong> value</div>
// plus a top banner with "Artists at Venue" and optional image.
function fieldValue(label) {
var rows = infobox.querySelectorAll('div');
for (var i = 0; i < rows.length; i++) {
var s = rows[i].querySelector('strong');
if (!s) continue;
var labelText = s.textContent.replace(/[:\s]*$/, '').trim();
if (labelText !== label) continue;
var clone = rows[i].cloneNode(true);
var strong = clone.querySelector('strong');
if (strong) strong.remove();
return clone.innerHTML.trim().replace(/^[\s:]+/, '');
}
return '';
}
var artists = fieldValue('Artists');
var venue = fieldValue('Venue');
var date = fieldValue('Date');
var showtime = fieldValue('Showtime');
var doors = fieldValue('Doors');
var show = fieldValue('Show');
var posterArtist = fieldValue('Poster art');
var tickets = fieldValue('Tickets');
var price = fieldValue('Price');
var ages = fieldValue('Ages');
var imageImg = infobox.querySelector('img');
var status = '';
if (infobox.classList.contains('show-verified')) status = 'verified';
else if (infobox.classList.contains('bot-proposal')) status = 'proposed';
else if (infobox.classList.contains('show-unverified')) status = 'unverified';
// ---------- Build the poster hero ----------
// Showtime is rendered by Template:Show as
// [https://etherscan.io/block/N N] — pull the block number out and
// format it with commas so the kicker reads "BLOCK 24,144,194".
function formatShowtime(htmlIn) {
if (!htmlIn) return '';
var tmp = document.createElement('div');
tmp.innerHTML = htmlIn;
var anchor = tmp.querySelector('a');
var blockNum = (anchor ? anchor.textContent : tmp.textContent).trim();
var asInt = parseInt(blockNum.replace(/[^\d]/g, ''), 10);
if (isNaN(asInt)) return blockNum;
var formatted = asInt.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
var href = anchor ? anchor.href : '';
if (href) {
return '<a href="' + href + '" target="_blank" rel="noopener">Block ' + formatted + '</a>';
}
return 'Block ' + formatted;
}
// Big date kicker (human-readable) on its own line, above the smaller
// technical kicker with block/doors/show.
var dateKickerHtml = date
? '<div class="pp-date-kicker">' + escapeHtml(date) + '</div>'
: '';
var kickerBits = [];
var showtimeHtml = formatShowtime(showtime);
if (showtimeHtml) kickerBits.push(showtimeHtml);
if (doors) kickerBits.push('Doors ' + doors);
if (show) kickerBits.push('Show ' + show);
var kickerHtml = kickerBits.length
? '<div class="pp-kicker">' + kickerBits.join('<span class="pp-sep">·</span>') + '</div>'
: '';
var venueHtml = venue ? '<p class="pp-venue">at ' + venue + '</p>' : '';
// Ensembles: find every Template:Ensemble block on the page. Each renders
// as a .ensemble-infobox with a colored header div (the band title) and
// a body div with one musician per line (via Template:m, which inlines
// a tiny instrument icon next to the linked musician name).
//
// We split on <br> and keep HTML chunks intact so the instrument icons
// and User: links survive the absorb. Each ensemble renders as two
// tiers: the band title on its own line (mono caps, accent), then the
// member list below in serif italic with rule-dot separators.
var ensembleBlocks = content.querySelectorAll('.ensemble-infobox');
var ensembleHtml = '';
ensembleBlocks.forEach(function (eb) {
var bandName = '';
var headerEl = eb.querySelector('div:first-child');
if (headerEl) bandName = headerEl.textContent.trim();
var listEl = eb.querySelector('div:nth-child(2)');
if (!listEl) return;
var members = listEl.innerHTML
.split(/<br\s*\/?>/i)
.map(function (chunk) { return chunk.trim(); })
.filter(Boolean);
if (!members.length) return;
var titleText = (bandName && bandName.toLowerCase() !== 'ensemble')
? bandName : 'Featuring';
ensembleHtml +=
'<div class="pp-ensemble">' +
'<p class="pp-ensemble-title">' + escapeHtml(titleText) + '</p>' +
'<p class="pp-ensemble-members">' +
members.join(' <span class="pp-sep">·</span> ') +
'</p>' +
'</div>';
eb.classList.add('is-absorbed');
});
var metaBits = [];
if (price)
metaBits.push('<span><span class="pp-meta-label">Price</span><span class="pp-meta-value">$' +
escapeHtml(price.replace(/^\$/, '')) + '</span></span>');
if (ages)
metaBits.push('<span><span class="pp-meta-label">Ages</span><span class="pp-meta-value">' +
ages + '</span></span>');
var metaHtml = metaBits.length
? '<div class="pp-meta">' + metaBits.join('') + '</div>'
: '';
// Upgrade the poster thumbnail: Template:Show renders [[File:foo|220px]]
// which gives us /images/thumb/.../220px-foo.png. We replace the 220px
// segment with 800px to request a larger thumbnail; CSS scales it to
// fit the card. If the URL doesn't match the thumb pattern (e.g. SVG,
// or non-thumbnailed file), we just use the original src.
var imageHtml = '';
if (imageImg) {
var bigSrc = imageImg.src.replace(/\/(\d+)px-/, '/800px-');
imageHtml = '<div class="pp-image"><img src="' + bigSrc +
'" alt="' + escapeHtml(imageImg.alt || '') + '">' +
(posterArtist
? '<p class="pp-poster-credit"><span class="pp-poster-credit-label">Poster art by</span> ' +
escapeHtml(posterArtist) + '</p>'
: '') +
'</div>';
}
// Tickets field is an anchor; surface its href as a poster action.
var actionBits = [];
if (tickets) {
var tmp = document.createElement('div'); tmp.innerHTML = tickets;
var ticketAnchor = tmp.querySelector('a');
if (ticketAnchor && ticketAnchor.href) {
actionBits.push('<a href="' + ticketAnchor.href +
'" target="_blank" rel="noopener">Tickets</a>');
}
}
var actionsHtml = actionBits.length
? '<div class="pp-actions">' + actionBits.join('') + '</div>'
: '';
var statusHtml = status
? '<div class="pp-status is-' + status + '">' + status + '</div>'
: '';
var titleHtml = artists || escapeHtml(title.replace(/^Show:\s*/, ''));
var hero = document.createElement('header');
hero.className = 'show-poster';
hero.innerHTML =
statusHtml +
dateKickerHtml +
kickerHtml +
'<h1 class="pp-title">' + titleHtml + '</h1>' +
venueHtml +
ensembleHtml +
metaHtml +
imageHtml +
actionsHtml;
content.insertBefore(hero, content.firstChild);
infobox.classList.add('is-absorbed');
body.classList.add('has-show-hero');
// ---------- Full-takeover chrome: corner logo + slim topbar ----------
// Only fires when the hero was built (Template:Show present). The CSS
// chrome-hiding rules are gated by .has-show-hero so bare Show: pages
// keep their wiki sidebar; if you want to make those takeover too,
// remove the .has-show-hero gating from the CSS file.
var LOGO_URL = '/images/thumb/8/80/Pickipedia-quarter-transparent.png/400px-Pickipedia-quarter-transparent.png';
var cornerLogo = document.createElement('a');
cornerLogo.className = 'pp-corner-logo';
cornerLogo.href = '/wiki/Main_Page';
cornerLogo.title = 'PickiPedia home';
cornerLogo.innerHTML = '<img src="' + LOGO_URL + '" alt="PickiPedia">';
body.insertBefore(cornerLogo, body.firstChild);
var topbar = document.createElement('header');
topbar.className = 'pickipedia-show-topbar';
topbar.innerHTML =
'<nav class="pickipedia-show-topbar-left">' +
'<a href="' + mw.util.getUrl('Category:Shows') + '">← All shows</a>' +
'</nav>' +
'<nav class="pickipedia-show-topbar-right">' +
'<a href="' + mw.util.getUrl(pageName, { action: 'edit' }) + '">edit</a>' +
'<a href="' + mw.util.getUrl(pageName, { action: 'history' }) + '">history</a>' +
'<a href="' + mw.util.getUrl('Talk:' + pageName) + '">talk</a>' +
'</nav>';
body.insertBefore(topbar, cornerLogo.nextSibling);
function escapeHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
}());