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) |
v6: wrap poster image in anchor to File: page (uses same href as infobox) |
||
| (3 intermediate revisions by the same user not shown) | |||
| Line 50: | Line 50: | ||
} | } | ||
var artists | var artists = fieldValue('Artists'); | ||
var venue | var venue = fieldValue('Venue'); | ||
var showtime = fieldValue('Showtime'); | var date = fieldValue('Date'); | ||
var doors | var showtime = fieldValue('Showtime'); | ||
var show | var doors = fieldValue('Doors'); | ||
var | var show = fieldValue('Show'); | ||
var tickets | var posterArtist = fieldValue('Poster art'); | ||
var price | var tickets = fieldValue('Tickets'); | ||
var ages | var price = fieldValue('Price'); | ||
var ages = fieldValue('Ages'); | |||
var imageImg = infobox.querySelector('img'); | var imageImg = infobox.querySelector('img'); | ||
| Line 86: | Line 87: | ||
return 'Block ' + formatted; | 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 kickerBits = []; | ||
| Line 99: | Line 106: | ||
var venueHtml = venue ? '<p class="pp-venue">at ' + venue + '</p>' : ''; | var venueHtml = venue ? '<p class="pp-venue">at ' + venue + '</p>' : ''; | ||
// Ensemble | // Ensembles ({{Ensemble|...}} blocks) are intentionally NOT absorbed into | ||
// | // the hero — they render in their source position on the page so the | ||
// wikitext author controls placement. The CSS in this gadget restyles | |||
// the existing .ensemble-infobox to fit the takeover aesthetic (right- | |||
// floating, paper-themed, mono-caps title, instrument icons inline). | |||
var ensembleHtml = ''; | var ensembleHtml = ''; | ||
var metaBits = []; | var metaBits = []; | ||
| Line 121: | Line 124: | ||
: ''; | : ''; | ||
// Upgrade the poster thumbnail: Template:Show renders [[File:foo|220px]] | |||
// which gives us /images/thumb/.../220px-foo.png inside an <a> linking | |||
// to the File: page. We grab that anchor's href so clicking the big | |||
// hero poster takes the user to the File: page (where the upload | |||
// history, full-res download, and license info live). The thumbnail | |||
// URL gets upsized from /220px-/ to /800px- for a sharper render; CSS | |||
// scales it to fit the card. SVGs and other non-thumbnailed files | |||
// keep their original src. | |||
var imageHtml = ''; | var imageHtml = ''; | ||
if (imageImg) { | if (imageImg) { | ||
var bigSrc = imageImg.src.replace(/\/(\d+)px-/, '/800px-'); | |||
'" | var imageAnchor = imageImg.closest('a'); | ||
var imageHref = imageAnchor ? imageAnchor.getAttribute('href') : ''; | |||
var imgTag = '<img src="' + bigSrc + '" alt="' + | |||
escapeHtml(imageImg.alt || '') + '">'; | |||
var wrapped = imageHref | |||
? '<a class="pp-image-link" href="' + imageHref + | |||
'" title="View file details">' + imgTag + '</a>' | |||
: imgTag; | |||
imageHtml = '<div class="pp-image">' + wrapped + | |||
(posterArtist | |||
? '<p class="pp-poster-credit"><span class="pp-poster-credit-label">Poster art by</span> ' + | |||
escapeHtml(posterArtist) + '</p>' | |||
: '') + | |||
'</div>'; | |||
} | } | ||
| Line 151: | Line 175: | ||
hero.innerHTML = | hero.innerHTML = | ||
statusHtml + | statusHtml + | ||
dateKickerHtml + | |||
kickerHtml + | kickerHtml + | ||
'<h1 class="pp-title">' + titleHtml + '</h1>' + | '<h1 class="pp-title">' + titleHtml + '</h1>' + | ||
venueHtml + | venueHtml + | ||
metaHtml + | metaHtml + | ||
imageHtml + | imageHtml + | ||
Latest revision as of 21:01, 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 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 ({{Ensemble|...}} blocks) are intentionally NOT absorbed into
// the hero — they render in their source position on the page so the
// wikitext author controls placement. The CSS in this gadget restyles
// the existing .ensemble-infobox to fit the takeover aesthetic (right-
// floating, paper-themed, mono-caps title, instrument icons inline).
var ensembleHtml = '';
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 inside an <a> linking
// to the File: page. We grab that anchor's href so clicking the big
// hero poster takes the user to the File: page (where the upload
// history, full-res download, and license info live). The thumbnail
// URL gets upsized from /220px-/ to /800px- for a sharper render; CSS
// scales it to fit the card. SVGs and other non-thumbnailed files
// keep their original src.
var imageHtml = '';
if (imageImg) {
var bigSrc = imageImg.src.replace(/\/(\d+)px-/, '/800px-');
var imageAnchor = imageImg.closest('a');
var imageHref = imageAnchor ? imageAnchor.getAttribute('href') : '';
var imgTag = '<img src="' + bigSrc + '" alt="' +
escapeHtml(imageImg.alt || '') + '">';
var wrapped = imageHref
? '<a class="pp-image-link" href="' + imageHref +
'" title="View file details">' + imgTag + '</a>'
: imgTag;
imageHtml = '<div class="pp-image">' + wrapped +
(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 +
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, '"');
}
}());