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
Magent (talk | contribs)
v5: stop absorbing {{Ensemble}} — let it render in source position
Magent (talk | contribs)
v6: wrap poster image in anchor to File: page (uses same href as infobox)
 
Line 125: Line 125:


   // Upgrade the poster thumbnail: Template:Show renders [[File:foo|220px]]
   // Upgrade the poster thumbnail: Template:Show renders [[File:foo|220px]]
   // which gives us /images/thumb/.../220px-foo.png. We replace the 220px
   // which gives us /images/thumb/.../220px-foo.png inside an <a> linking
   // segment with 800px to request a larger thumbnail; CSS scales it to
  // to the File: page. We grab that anchor's href so clicking the big
   // fit the card. If the URL doesn't match the thumb pattern (e.g. SVG,
  // hero poster takes the user to the File: page (where the upload
   // or non-thumbnailed file), we just use the original src.
  // 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 bigSrc = imageImg.src.replace(/\/(\d+)px-/, '/800px-');
     imageHtml = '<div class="pp-image"><img src="' + bigSrc +
     var imageAnchor = imageImg.closest('a');
      '" alt="' + escapeHtml(imageImg.alt || '') + '">' +
    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
       (posterArtist
         ? '<p class="pp-poster-credit"><span class="pp-poster-credit-label">Poster art by</span> ' +
         ? '<p class="pp-poster-credit"><span class="pp-poster-credit-label">Poster art by</span> ' +

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, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;');
  }
}());