Candidate:Gadget-pickipedia-show.js: Difference between revisions
From PickiPedia: A knowledge base of bluegrass, old time psychedelic jams, and other public domain music
Jump to navigationJump to search
Full-takeover mode: insert corner logo + slim topbar (edit/history/talk/← All shows) |
v3: absorb {{Ensemble|...}} blocks (not ensemble= field); upgrade poster thumb to 800px; render poster_artist credit |
||
| Line 50: | Line 50: | ||
} | } | ||
var artists | var artists = fieldValue('Artists'); | ||
var venue | var venue = fieldValue('Venue'); | ||
var showtime = fieldValue('Showtime'); | var showtime = fieldValue('Showtime'); | ||
var doors | var doors = fieldValue('Doors'); | ||
var show | var show = fieldValue('Show'); | ||
var | var posterArtist = fieldValue('Poster art'); | ||
var tickets | var tickets = fieldValue('Tickets'); | ||
var price | var price = fieldValue('Price'); | ||
var ages | var ages = fieldValue('Ages'); | ||
var imageImg = infobox.querySelector('img'); | var imageImg = infobox.querySelector('img'); | ||
| Line 99: | Line 99: | ||
var venueHtml = venue ? '<p class="pp-venue">at ' + venue + '</p>' : ''; | 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). Multiple | |||
// ensembles per page are common — headliner + opener(s) + sit-ins. | |||
var ensembleBlocks = content.querySelectorAll('.ensemble-infobox'); | |||
var ensembleHtml = ''; | var ensembleHtml = ''; | ||
if ( | ensembleBlocks.forEach(function (eb) { | ||
var | // Header div carries the band name (or default "Ensemble"). | ||
if ( | var bandName = ''; | ||
var headerEl = eb.querySelector('div:first-child'); | |||
if (headerEl) bandName = headerEl.textContent.trim(); | |||
} | // Member list: the second-child div has musicians separated by <br>. | ||
var listEl = eb.querySelector('div:nth-child(2)'); | |||
if (!listEl) return; | |||
// Each "row" is text between <br>s; pull text content and split. | |||
var raw = listEl.innerHTML | |||
.split(/<br\s*\/?>/i) | |||
.map(function (chunk) { | |||
var tmp = document.createElement('div'); | |||
tmp.innerHTML = chunk; | |||
return tmp.textContent.trim(); | |||
}) | |||
.filter(Boolean); | |||
if (!raw.length) return; | |||
var labelHtml = bandName && bandName.toLowerCase() !== 'ensemble' | |||
? '<span class="pp-ensemble-label">' + escapeHtml(bandName) + '</span>' | |||
: '<span class="pp-ensemble-label">Featuring</span>'; | |||
ensembleHtml += '<p class="pp-ensemble">' + labelHtml + ' ' + | |||
raw.map(escapeHtml).join(' <span class="pp-sep">·</span> ') + '</p>'; | |||
eb.classList.add('is-absorbed'); | |||
}); | |||
var metaBits = []; | var metaBits = []; | ||
| Line 121: | Line 145: | ||
: ''; | : ''; | ||
// 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 = ''; | var imageHtml = ''; | ||
if (imageImg) { | if (imageImg) { | ||
imageHtml = '<div class="pp-image"><img src="' + | var bigSrc = imageImg.src.replace(/\/(\d+)px-/, '/800px-'); | ||
'" alt="' + escapeHtml(imageImg.alt || '') + '"></div>'; | 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>'; | |||
} | } | ||
Revision as of 20:18, 2 June 2026
// |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 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;
}
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). Multiple
// ensembles per page are common — headliner + opener(s) + sit-ins.
var ensembleBlocks = content.querySelectorAll('.ensemble-infobox');
var ensembleHtml = '';
ensembleBlocks.forEach(function (eb) {
// Header div carries the band name (or default "Ensemble").
var bandName = '';
var headerEl = eb.querySelector('div:first-child');
if (headerEl) bandName = headerEl.textContent.trim();
// Member list: the second-child div has musicians separated by <br>.
var listEl = eb.querySelector('div:nth-child(2)');
if (!listEl) return;
// Each "row" is text between <br>s; pull text content and split.
var raw = listEl.innerHTML
.split(/<br\s*\/?>/i)
.map(function (chunk) {
var tmp = document.createElement('div');
tmp.innerHTML = chunk;
return tmp.textContent.trim();
})
.filter(Boolean);
if (!raw.length) return;
var labelHtml = bandName && bandName.toLowerCase() !== 'ensemble'
? '<span class="pp-ensemble-label">' + escapeHtml(bandName) + '</span>'
: '<span class="pp-ensemble-label">Featuring</span>';
ensembleHtml += '<p class="pp-ensemble">' + labelHtml + ' ' +
raw.map(escapeHtml).join(' <span class="pp-sep">·</span> ') + '</p>';
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 +
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, '"');
}
}());