MediaWiki: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
No edit summary
No edit summary
 
(One intermediate revision by the same user not shown)
Line 105: Line 105:
   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
   // Ensembles ({{Ensemble|...}} blocks) are intentionally NOT absorbed into
  // as a .ensemble-infobox with a colored header div (the band title) and
   // the hero — they render in their source position on the page so the
   // a body div with one musician per line (via Template:m, which inlines
   // wikitext author controls placement. The CSS in this gadget restyles
  // a tiny instrument icon next to the linked musician name).
   // the existing .ensemble-infobox to fit the takeover aesthetic (right-
  //
   // floating, paper-themed, mono-caps title, instrument icons inline).
  // 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 = '';
   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 = [];
   var metaBits = [];
Line 156: Line 124:


   // 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> ' +
Line 200: Line 178:
     '<h1 class="pp-title">' + titleHtml + '</h1>' +
     '<h1 class="pp-title">' + titleHtml + '</h1>' +
     venueHtml +
     venueHtml +
    ensembleHtml +
     metaHtml +
     metaHtml +
     imageHtml +
     imageHtml +

Latest revision as of 21:02, 2 June 2026

/**
 * 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;');
  }
}());