MediaWiki:Common-mainpage.js: Difference between revisions
MediaWiki interface page
More actions
No edit summary |
Joe Beaudoin Jr. changed the content model of the page MediaWiki:Common-mainpage.js from "wikitext" to "JavaScript" |
||
| (10 intermediate revisions by the same user not shown) | |||
| Line 18: | Line 18: | ||
var API = 'https://en.battlestarwiki.org/w/api.php'; | var API = 'https://en.battlestarwiki.org/w/api.php'; | ||
var FR_API = 'https://fr.battlestarwiki.ddns.net/api.php'; | |||
var MEDIA_API = 'https://media.battlestarwiki.org/w/api.php'; | var MEDIA_API = 'https://media.battlestarwiki.org/w/api.php'; | ||
var DE_API = 'https://de.battlestarwiki.org/w/api.php'; | var DE_API = 'https://de.battlestarwiki.org/w/api.php'; | ||
| Line 107: | Line 108: | ||
var badgeStyle = series.color ? ' style="background:' + series.color + '"' : ''; | var badgeStyle = series.color ? ' style="background:' + series.color + '"' : ''; | ||
return '<div class="bsw-slide bsw-active" data-article="' + esc( title ) + '">' + | return '<div class="bsw-slide bsw-active" data-article="' + esc( title ) + '" data-url="' + esc( url ) + '">' + | ||
'<div class="bsw-slide-bg"></div>' + | '<div class="bsw-slide-bg"></div>' + | ||
'<div class="bsw-slide-overlay"></div>' + | '<div class="bsw-slide-overlay"></div>' + | ||
| Line 158: | Line 159: | ||
window.bswPrevSlide = function () { goTo( current - 1 ); }; | window.bswPrevSlide = function () { goTo( current - 1 ); }; | ||
window.bswNextSlide = function () { goTo( current + 1 ); }; | window.bswNextSlide = function () { goTo( current + 1 ); }; | ||
/* Click on image area (overlay or bg) navigates to article */ | |||
slides.forEach( function ( slide ) { | |||
[ '.bsw-slide-overlay', '.bsw-slide-bg' ].forEach( function ( sel ) { | |||
var el = slide.querySelector( sel ); | |||
if ( !el ) return; | |||
el.style.cursor = 'pointer'; | |||
el.addEventListener( 'click', function () { | |||
var url = slide.getAttribute( 'data-url' ); | |||
if ( url ) window.location.href = url; | |||
} ); | |||
} ); | |||
} ); | |||
dots.forEach( function ( dot, i ) { | dots.forEach( function ( dot, i ) { | ||
| Line 427: | Line 441: | ||
var thumbHtml; | var thumbHtml; | ||
if ( thumb ) { | if ( thumb ) { | ||
thumbHtml = '<img class="bsw-fa-thumb" src="' + esc( thumb ) + '" alt="' + esc( title ) + '">'; | thumbHtml = '<img class="bsw-fa-thumb" src="' + esc( thumb ) + '" alt="' + esc( title ) + '" width="90" height="110">'; | ||
} else { | } else { | ||
thumbHtml = '<img class="bsw-fa-thumb" src="https://en.battlestarwiki.org/resources/BSGWIKILOGO.png" alt="Battlestar Wiki">'; | thumbHtml = '<img class="bsw-fa-thumb" src="https://en.battlestarwiki.org/resources/BSGWIKILOGO.png" alt="Battlestar Wiki" width="90" height="110">'; | ||
} | } | ||
| Line 502: | Line 516: | ||
wrap.innerHTML = '<p style="color:var(--color-base--subtle);font-size:0.8125rem;padding:0.5rem 0">Loading\u2026</p>'; | wrap.innerHTML = '<p style="color:var(--color-base--subtle);font-size:0.8125rem;padding:0.5rem 0">Loading\u2026</p>'; | ||
var apiMap = { en: API, de: DE_API, media: MEDIA_API }; | var apiMap = { en: API, de: DE_API, fr: FR_API, media: MEDIA_API }; | ||
var base = apiMap[ wiki ]; | var base = apiMap[ wiki ]; | ||
if ( !base ) return; | if ( !base ) return; | ||
var nsMap = { | |||
en: '0', | |||
de: '0', | |||
fr: '0', | |||
media: '0|6' | |||
}; | |||
apiGetFrom( base, { | apiGetFrom( base, { | ||
| Line 510: | Line 531: | ||
list: 'recentchanges', | list: 'recentchanges', | ||
rcprop: 'title|timestamp|user|sizes', | rcprop: 'title|timestamp|user|sizes', | ||
rcnamespace: '0', | rcnamespace: nsMap[ wiki ] || '0', | ||
rclimit: ' | rclimit: '10', | ||
rctype: 'edit|new' | rctype: 'edit|new' | ||
} ).then( function ( data ) { | } ).then( function ( data ) { | ||
var changes = data.query.recentchanges || []; | var changes = ( data.query.recentchanges || [] ).filter( function ( rc ) { | ||
/* Filter out main page and subpages */ | |||
return rc.title !== 'Main Page' && rc.title.indexOf( '/' ) === -1; | |||
} ); | |||
changes = changes.filter( function ( rc ) { | |||
return rc.title !== 'Main Page' && rc.title.indexOf( '/' ) === -1; | |||
} ); | |||
if ( !changes.length ) { | if ( !changes.length ) { | ||
wrap.innerHTML = '<p style="color:var(--color-base--subtle);font-size:0.8125rem;padding:0.5rem 0">No recent changes.</p>'; | wrap.innerHTML = '<p style="color:var(--color-base--subtle);font-size:0.8125rem;padding:0.5rem 0">No recent changes.</p>'; | ||
| Line 521: | Line 548: | ||
var baseUrl = { | var baseUrl = { | ||
en: 'https://en.battlestarwiki.org | en: 'https://en.battlestarwiki.org/', | ||
de: 'https://de.battlestarwiki.org/wiki/', | de: 'https://de.battlestarwiki.org/', | ||
media: 'https://media.battlestarwiki.org | fr: 'https://fr.battlestarwiki.ddns.net/wiki/', | ||
media: 'https://media.battlestarwiki.org/' | |||
}[ wiki ]; | }[ wiki ]; | ||
var isCrosswiki = ( wiki !== 'all' && wiki !== 'en' ); | |||
/* Deduplicate by title */ | |||
var seen = {}; | |||
changes = changes.filter( function ( rc ) { | |||
if ( seen[ rc.title ] ) return false; | |||
seen[ rc.title ] = true; | |||
return true; | |||
} ); | |||
var html = '<ul class="bsw-recent">' + changes.map( function ( rc ) { | var html = '<ul class="bsw-recent">' + changes.map( function ( rc ) { | ||
var title = rc.title; | var title = rc.title; | ||
var url | var displayTitle = title.replace( /^[^:]+:/, '' ); /* strip namespace prefix */ | ||
var ts | var url = baseUrl + encodeURIComponent( title.replace( / /g, '_' ) ); | ||
var ago | var ts = new Date( rc.timestamp ); | ||
var ago = timeAgo( ts ); | |||
var cls = isCrosswiki ? ' class="external"' : ''; | |||
var target = isCrosswiki ? ' target="_blank"' : ''; | |||
return '<li class="ri">' + | return '<li class="ri">' + | ||
'<a href="' + esc( url ) + '" target | '<a href="' + esc( url ) + '"' + cls + target + '>' + esc( displayTitle ) + '</a>' + | ||
'<span class="bsw-recent-time">' + esc( ago ) + '</span>' + | '<span class="bsw-recent-time">' + esc( ago ) + '</span>' + | ||
'</li>'; | '</li>'; | ||
| Line 548: | Line 589: | ||
if ( diff < 60 ) return diff + 's ago'; | if ( diff < 60 ) return diff + 's ago'; | ||
if ( diff < 3600 ) return Math.floor( diff / 60 ) + ' min ago'; | if ( diff < 3600 ) return Math.floor( diff / 60 ) + ' min ago'; | ||
var hrs = Math.floor( diff / 3600 ); | |||
if ( diff < 86400 ) return hrs + ( hrs === 1 ? ' hr ago' : ' hrs ago' ); | |||
var days = Math.floor( diff / 86400 ); | |||
return days + ( days === 1 ? ' day ago' : ' days ago' ); | |||
} | } | ||
| Line 585: | Line 628: | ||
prop: 'extracts|pageimages|pageprops', | prop: 'extracts|pageimages|pageprops', | ||
exintro: '1', | exintro: '1', | ||
exchars: ' | exchars: '500', | ||
exsectionformat: 'plain', | exsectionformat: 'plain', | ||
piprop: 'thumbnail', | piprop: 'thumbnail', | ||
| Line 618: | Line 661: | ||
var thumbHtml = thumb | var thumbHtml = thumb | ||
? '<img src="' + esc( thumb ) + '" alt="' + esc( title ) + '" class="bsw-fa-thumb">' | ? '<img src="' + esc( thumb ) + '" alt="' + esc( title ) + '" class="bsw-fa-thumb" width="90" height="110">' | ||
: '<img src="https://en.battlestarwiki.org/resources/BSGWIKILOGO.png" alt="Battlestar Wiki" class="bsw-fa-thumb">'; | : '<img src="https://en.battlestarwiki.org/resources/BSGWIKILOGO.png" alt="Battlestar Wiki" class="bsw-fa-thumb" width="90" height="110">'; | ||
/* Clean up extract — | /* Clean up extract — truncate at sentence boundary */ | ||
var cleanExtract = extract.replace( /\s*\.\.\.\s*$/, '' ); | var cleanExtract = extract.replace( /\s*\.\.\.\s*$/, '' ); | ||
/* Try to truncate at last sentence end within ~400 chars */ | |||
var stripped = cleanExtract.replace( /<[^>]+>/g, '' ); | |||
if ( stripped.length > 400 ) { | |||
var lastPeriod = cleanExtract.lastIndexOf( '.', 500 ); | |||
if ( lastPeriod > 200 ) { | |||
cleanExtract = cleanExtract.slice( 0, lastPeriod + 1 ); | |||
} | |||
} | |||
inner.innerHTML = | inner.innerHTML = | ||
| Line 685: | Line 736: | ||
} | } | ||
/* bswSetTab — exposed globally for inline onclick | /* bswSetTab — exposed globally as fallback for inline onclick */ | ||
window.bswSetTab = function ( el, wiki ) { | window.bswSetTab = function ( el, wiki ) { | ||
document.querySelectorAll( '.bsw-wtab' ).forEach( function ( t ) { | document.querySelectorAll( '.bsw-wtab' ).forEach( function ( t ) { | ||
| Line 694: | Line 745: | ||
loadRecentChanges( wiki ); | loadRecentChanges( wiki ); | ||
}; | }; | ||
function wireRcTabs() { | |||
var tabMap = { 'All': 'all', 'EN': 'en', 'DE': 'de', 'FR': 'fr', 'Media': 'media' }; | |||
document.querySelectorAll( '.bsw-wtab' ).forEach( function ( tab ) { | |||
var wiki = tabMap[ tab.textContent.trim() ]; | |||
if ( !wiki ) return; | |||
tab.addEventListener( 'click', function () { | |||
document.querySelectorAll( '.bsw-wtab' ).forEach( function ( t ) { | |||
t.classList.remove( 'bsw-active' ); | |||
} ); | |||
tab.classList.add( 'bsw-active' ); | |||
rcCurrentWiki = wiki; | |||
loadRecentChanges( wiki ); | |||
} ); | |||
} ); | |||
} | |||
mw.hook( 'wikipage.content' ).add( function () { | mw.hook( 'wikipage.content' ).add( function () { | ||
| Line 702: | Line 769: | ||
initNewestArticle(); | initNewestArticle(); | ||
filterRedlinkCards(); | filterRedlinkCards(); | ||
wireRcTabs(); | |||
/* Cache the initial 'all' RC content so tab switching can restore it */ | /* Cache the initial 'all' RC content so tab switching can restore it */ | ||
var rcWrap = document.getElementById( 'bsw-rc-content' ); | var rcWrap = document.getElementById( 'bsw-rc-content' ); | ||
Latest revision as of 03:17, 13 April 2026
/**
* BattlestarWiki — Main Page JavaScript
* Append to MediaWiki:Common.js
*
* Handles:
* 1. Hero slideshow auto-rotation + article image fetching
* 2. Featured Article of the Day — daily deterministic pick,
* excluding Category:Stub_Pages, with extract + thumbnail
* 3. Media wiki file count (from media.battlestarwiki.org)
* 4. Recent changes tab switching (EN / DE / Media wikis)
* 5. Red-link card filtering
*
* All logic is scoped to .bsw-main-page; safe to load globally.
*/
( function () {
'use strict';
var API = 'https://en.battlestarwiki.org/w/api.php';
var FR_API = 'https://fr.battlestarwiki.ddns.net/api.php';
var MEDIA_API = 'https://media.battlestarwiki.org/w/api.php';
var DE_API = 'https://de.battlestarwiki.org/w/api.php';
/* ── Shared utilities ─────────────────────────────────────────── */
function dailySeed( dateStr ) {
var d;
if ( dateStr ) {
var p = dateStr.split( '-' );
d = Date.UTC( +p[0], +p[1] - 1, +p[2] );
} else {
var now = new Date();
d = Date.UTC( now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() );
}
return Math.floor( d / 86400000 );
}
function esc( s ) {
return String( s || '' )
.replace( /&/g, '&' )
.replace( /</g, '<' )
.replace( />/g, '>' )
.replace( /"/g, '"' );
}
function apiGetFrom( baseUrl, params ) {
params.format = 'json';
params.origin = '*';
var qs = Object.keys( params )
.map( function ( k ) { return encodeURIComponent( k ) + '=' + encodeURIComponent( params[k] ); } )
.join( '&' );
return fetch( baseUrl + '?' + qs, { headers: { Accept: 'application/json' } } )
.then( function ( r ) {
if ( !r.ok ) throw new Error( 'API HTTP ' + r.status );
return r.json();
} );
}
function apiGet( params ) {
return apiGetFrom( API, params );
}
/* ════════════════════════════════════════════════════════════════
1. HERO SLIDESHOW
Dynamically populated from the wiki's most-viewed articles.
Uses a daily seed so all visitors see the same 3 slides each day.
Falls back to the static Lua-rendered slides if the API fails.
════════════════════════════════════════════════════════════════ */
/* Series classification for badge color + label */
var SERIES_CATS = [
{ pat: /\(RDM\)|\(TRS\)|Re-imagined|Reimagined/i, label: 'Re-imagined Series', color: 'var(--color-primary)' },
{ pat: /\(TOS\)|Original Series|\bTOS\b/i, label: 'Original Series', color: '#ef9f27' },
{ pat: /\(1980\)|Galactica.?1980|Category:1980\b/i, label: 'Galactica 1980', color: '#97c459' },
{ pat: /\(Caprica\)|Caprica \(series\)|Category:Caprica\b/i, label: 'Caprica', color: '#d4537e' },
{ pat: /\(BAC\)|Blood and Chrome|Blood.*Chrome|\bBAC\b/i, label: 'Blood & Chrome', color: '#e24b4a' }
];
function classifyArticle( title, cats, extract ) {
/* Check category names first */
if ( cats ) {
for ( var i = 0; i < SERIES_CATS.length; i++ ) {
for ( var j = 0; j < cats.length; j++ ) {
if ( SERIES_CATS[i].pat.test( cats[j].title ) ) return SERIES_CATS[i];
}
}
}
/* Fall back to title matching */
for ( var k = 0; k < SERIES_CATS.length; k++ ) {
if ( SERIES_CATS[k].pat.test( title ) ) return SERIES_CATS[k];
}
/* Final fallback: scan extract for series keywords */
if ( extract ) {
var text = extract.replace( /<[^>]+>/g, '' );
for ( var m = 0; m < SERIES_CATS.length; m++ ) {
if ( SERIES_CATS[m].pat.test( text ) ) return SERIES_CATS[m];
}
}
return { label: 'Battlestar Wiki', color: 'var(--color-primary)' };
}
function buildSlideHTML( article, series, imgUrl ) {
var title = article.title;
var url = '/wiki/' + encodeURIComponent( title.replace( / /g, '_' ) );
var desc = ( article.extract || '' ).replace( /<[^>]+>/g, '' ).slice( 0, 160 );
if ( desc.length === 160 ) desc = desc.replace( /\s\S+$/, '' ) + '…';
var badgeStyle = series.color ? ' style="background:' + series.color + '"' : '';
return '<div class="bsw-slide bsw-active" data-article="' + esc( title ) + '" data-url="' + esc( url ) + '">' +
'<div class="bsw-slide-bg"></div>' +
'<div class="bsw-slide-overlay"></div>' +
'<div class="bsw-slide-content">' +
'<div class="bsw-slide-badge">' +
'<span class="bsw-slide-badge-dot"' + badgeStyle + '></span>' +
'<span>' + esc( series.label ) + '</span>' +
'</div>' +
'<div class="bsw-slide-title"><a href="' + esc( url ) + '">' + esc( title ) + '</a></div>' +
'<div class="bsw-slide-desc">' + esc( desc ) + '</div>' +
'</div>' +
'</div>';
}
function buildDotsAndNav( count ) {
var dots = '<div class="bsw-hero-dots">';
for ( var i = 0; i < count; i++ ) {
var cls = 'bsw-hero-dot' + ( i === 0 ? ' bsw-active' : '' );
dots += '<span class="' + cls + '" role="button" tabindex="0" onclick="bswGoSlide(' + i + ')"></span>';
}
dots += '</div>';
var nav = '<div class="bsw-hero-nav">' +
'<span class="bsw-hero-btn" role="button" tabindex="0" id="bsw-hero-prev" onclick="bswPrevSlide()">‹</span>' +
'<span class="bsw-hero-btn" role="button" tabindex="0" id="bsw-hero-next" onclick="bswNextSlide()">›</span>' +
'</div>';
return dots + nav;
}
function initHeroSlideshow( hero, slides ) {
var dots = hero.querySelectorAll( '.bsw-hero-dot' );
var current = 0;
var total = slides.length;
var timer;
function goTo( n ) {
slides[ current ].classList.remove( 'bsw-active' );
dots[ current ] && dots[ current ].classList.remove( 'bsw-active' );
current = ( n + total ) % total;
slides[ current ].classList.add( 'bsw-active' );
dots[ current ] && dots[ current ].classList.add( 'bsw-active' );
resetTimer();
}
function resetTimer() {
clearInterval( timer );
timer = setInterval( function () { goTo( current + 1 ); }, 6000 );
}
window.bswGoSlide = function ( n ) { goTo( n ); };
window.bswPrevSlide = function () { goTo( current - 1 ); };
window.bswNextSlide = function () { goTo( current + 1 ); };
/* Click on image area (overlay or bg) navigates to article */
slides.forEach( function ( slide ) {
[ '.bsw-slide-overlay', '.bsw-slide-bg' ].forEach( function ( sel ) {
var el = slide.querySelector( sel );
if ( !el ) return;
el.style.cursor = 'pointer';
el.addEventListener( 'click', function () {
var url = slide.getAttribute( 'data-url' );
if ( url ) window.location.href = url;
} );
} );
} );
dots.forEach( function ( dot, i ) {
dot.addEventListener( 'click', function () { goTo( i ); } );
} );
var prevBtn = document.getElementById( 'bsw-hero-prev' );
var nextBtn = document.getElementById( 'bsw-hero-next' );
if ( prevBtn ) prevBtn.addEventListener( 'click', function () { goTo( current - 1 ); } );
if ( nextBtn ) nextBtn.addEventListener( 'click', function () { goTo( current + 1 ); } );
hero.addEventListener( 'mouseenter', function () { clearInterval( timer ); } );
hero.addEventListener( 'mouseleave', resetTimer );
resetTimer();
}
function fetchSlideImages( hero, slides ) {
var titles = Array.from( slides ).map( function ( s ) { return s.dataset.article; } ).filter( Boolean );
if ( !titles.length ) return;
apiGet( {
action: 'query',
titles: titles.join( '|' ),
prop: 'pageimages',
piprop: 'thumbnail|original',
pithumbsize: '1200',
redirects: '1'
} ).then( function ( data ) {
var pages = data.query.pages;
var imageMap = {};
var normalized = {};
if ( data.query.normalized ) {
data.query.normalized.forEach( function ( n ) { normalized[ n.to ] = n.from; } );
}
var redirects = {};
if ( data.query.redirects ) {
data.query.redirects.forEach( function ( r ) { redirects[ r.to ] = r.from; } );
}
Object.values( pages ).forEach( function ( page ) {
if ( !page.title ) return;
var img = ( page.original && page.original.source ) ||
( page.thumbnail && page.thumbnail.source ) || '';
if ( !img ) return;
imageMap[ page.title ] = img;
if ( redirects[ page.title ] ) imageMap[ redirects[ page.title ] ] = img;
if ( normalized[ page.title ] ) imageMap[ normalized[ page.title ] ] = img;
} );
slides.forEach( function ( slide ) {
var imgUrl = imageMap[ slide.dataset.article ];
if ( !imgUrl ) return;
var bg = slide.querySelector( '.bsw-slide-bg' );
if ( !bg ) return;
var bgImg = document.createElement( 'img' );
bgImg.src = imgUrl;
bgImg.alt = '';
bgImg.className = 'bsw-slide-bg-blur';
bgImg.setAttribute( 'aria-hidden', 'true' );
var fgImg = document.createElement( 'img' );
fgImg.src = imgUrl;
fgImg.alt = '';
fgImg.className = 'bsw-slide-bg-img';
bg.appendChild( bgImg );
bg.appendChild( fgImg );
} );
} ).catch( function () {} );
}
function initHero() {
var hero = document.querySelector( '.bsw-hero' );
if ( !hero ) return;
var seed = dailySeed();
var SLIDE_COUNT = 3;
var FETCH_BATCH = 20; /* MW caps rnlimit at 20 for non-bots */
/* Fetch two batches of random articles for a larger candidate pool */
Promise.all( [
apiGet( { action: 'query', list: 'random', rnnamespace: '0', rnlimit: String( FETCH_BATCH ) } ),
apiGet( { action: 'query', list: 'random', rnnamespace: '0', rnlimit: String( FETCH_BATCH ) } )
] ).then( function ( results ) {
var combined = results[0].query.random.concat( results[1].query.random );
/* Deduplicate and filter */
var seen = {};
var items = combined.filter( function ( p ) {
if ( seen[ p.title ] ) return false;
seen[ p.title ] = true;
return p.title !== 'Main Page' &&
p.title !== 'Main Page/New' &&
p.title.indexOf( '/' ) === -1;
} );
if ( !items.length ) throw new Error( 'No articles' );
/* Cap at 20 titles to stay within MW API limits */
items = items.slice( 0, 20 );
/* Fetch extracts + images + categories for all candidates in one call */
return apiGet( {
action: 'query',
titles: items.map( function ( p ) { return p.title; } ).join( '|' ),
prop: 'extracts|pageimages|categories|pageprops',
exintro: '1',
exchars: '200',
exsectionformat: 'plain',
piprop: 'thumbnail|original',
pithumbsize: '1200',
cllimit: '500',
ppprop: 'disambiguation',
redirects: '1'
} );
} ).then( function ( data ) {
var pages = Object.values( data.query.pages || {} );
/* Keep only articles that have an image and aren't disambiguation pages */
var withImages = pages.filter( function ( p ) {
if ( !p.thumbnail && !p.original ) return false;
var cats = ( p.categories || [] ).map( function ( c ) { return c.title; } );
var isDisamb = ( p.pageprops && p.pageprops.disambiguation !== undefined ) ||
cats.some( function ( c ) { return c === 'Category:Disambiguation'; } );
var isStub = cats.some( function ( c ) { return /[Ss]tub/.test( c ); } );
return !isDisamb && !isStub;
} );
/* Fall back to all non-disambiguation pages if not enough have images */
var pool = withImages.length >= SLIDE_COUNT ? withImages : pages.filter( function ( p ) {
var cats = ( p.categories || [] ).map( function ( c ) { return c.title; } );
var isDisamb = ( p.pageprops && p.pageprops.disambiguation !== undefined ) ||
cats.some( function ( c ) { return c === 'Category:Disambiguation'; } );
var isStub = cats.some( function ( c ) { return /[Ss]tub/.test( c ); } );
return !isDisamb && !isStub;
} );
if ( pool.length < SLIDE_COUNT ) throw new Error( 'Not enough articles' );
/* Pick SLIDE_COUNT articles using daily seed with spacing */
var picks = [];
for ( var i = 0; i < SLIDE_COUNT; i++ ) {
picks.push( pool[ ( seed + i * 7 ) % pool.length ] );
}
/* Build slide HTML */
var slideHTML = picks.map( function ( article, i ) {
var cats = article.categories || [];
var series = classifyArticle( article.title, cats, article.extract );
var html = buildSlideHTML( article, series, '' );
return i === 0 ? html : html.replace( 'bsw-slide bsw-active', 'bsw-slide' );
} ).join( '' );
hero.innerHTML = slideHTML + buildDotsAndNav( picks.length );
var newSlides = hero.querySelectorAll( '.bsw-slide' );
initHeroSlideshow( hero, newSlides );
/* Inject images directly from the data we already fetched */
picks.forEach( function ( article, i ) {
var imgUrl = ( article.original && article.original.source ) ||
( article.thumbnail && article.thumbnail.source ) || '';
if ( !imgUrl ) return;
var bg = newSlides[ i ] && newSlides[ i ].querySelector( '.bsw-slide-bg' );
if ( !bg ) return;
var bgImg = document.createElement( 'img' );
bgImg.src = imgUrl;
bgImg.alt = '';
bgImg.className = 'bsw-slide-bg-blur';
bgImg.setAttribute( 'aria-hidden', 'true' );
var fgImg = document.createElement( 'img' );
fgImg.src = imgUrl;
fgImg.alt = '';
fgImg.className = 'bsw-slide-bg-img';
bg.appendChild( bgImg );
bg.appendChild( fgImg );
} );
} ).catch( function ( err ) {
/* Fallback: use static Lua-rendered slides */
var staticSlides = hero.querySelectorAll( '.bsw-slide' );
if ( staticSlides.length ) {
/* Ensure first slide is active */
staticSlides.forEach( function( s ) { s.classList.remove( 'bsw-active' ); } );
staticSlides[0].classList.add( 'bsw-active' );
initHeroSlideshow( hero, staticSlides );
fetchSlideImages( hero, staticSlides );
}
} );
}
/* ════════════════════════════════════════════════════════════════
2. FEATURED ARTICLE OF THE DAY
Daily deterministic pick from all mainspace articles,
excluding Category:Stub_Pages.
════════════════════════════════════════════════════════════════ */
function initFeatured() {
var container = document.getElementById( 'bsw-featured-inner' );
if ( !container ) return;
var seed = dailySeed();
/* We fetch batches of 20 random articles, check for stubs,
and pick the first clean one. Two batches covers ~99.99%
of cases given only ~9% of articles are stubs. */
fetchCandidateBatch( seed )
.then( function ( pages ) {
var clean = pages.filter( function ( p ) {
/* If categories array is present and contains Stub_Pages,
exclude. Absence of categories key means not a stub. */
if ( !p.categories ) return true;
return !p.categories.some( function ( c ) {
return c.title === 'Category:Stub_Pages';
} );
} );
if ( !clean.length ) {
/* Extremely unlikely — fall back to second batch */
return fetchCandidateBatch( seed + 1 );
}
return clean;
} )
.then( function ( pages ) {
/* Pick deterministically from the clean pool */
var pick = pages[ seed % pages.length ];
renderFeatured( container, pick );
} )
.catch( function ( e ) {
container.innerHTML =
'<div class="bsw-error">Could not load featured article. ' + esc( e.message ) + '</div>';
} );
}
/**
* Fetch 20 random mainspace articles with extract, thumbnail,
* and stub-category membership check in a single API call.
* Uses rnstart derived from seed for determinism.
*/
function fetchCandidateBatch( seed ) {
/* Step 1: get 20 random page IDs */
return apiGet( {
action: 'query',
list: 'random',
rnnamespace: '0',
rnlimit: '20'
} ).then( function ( data ) {
var ids = data.query.random.map( function ( p ) { return p.id; } ).join( '|' );
/* Step 2: batch fetch extract + thumbnail + stub check */
return apiGet( {
action: 'query',
pageids: ids,
prop: 'extracts|pageimages|categories',
exintro: '1',
exchars: '600',
exsectionformat: 'plain',
piprop: 'thumbnail',
pithumbsize: '320',
clcategories: 'Category:Stub_Pages',
cllimit: '1'
} );
} ).then( function ( data ) {
return Object.values( data.query.pages );
} );
}
function renderFeatured( container, page ) {
var title = page.title || '';
var url = 'https://en.battlestarwiki.org/wiki/' + encodeURIComponent( title.replace( / /g, '_' ) );
var extract = page.extract || '';
var thumb = page.thumbnail ? page.thumbnail.source : '';
var thumbHtml;
if ( thumb ) {
thumbHtml = '<img class="bsw-fa-thumb" src="' + esc( thumb ) + '" alt="' + esc( title ) + '" width="90" height="110">';
} else {
thumbHtml = '<img class="bsw-fa-thumb" src="https://en.battlestarwiki.org/resources/BSGWIKILOGO.png" alt="Battlestar Wiki" width="90" height="110">';
}
container.innerHTML =
thumbHtml +
'<div>' +
'<div class="bsw-fa-title">' +
'<a href="' + esc( url ) + '">' + esc( title ) + '</a>' +
'</div>' +
'<div class="bsw-fa-extract">' + extract + '</div>' +
'<div style="margin-top:0.375rem">' +
'<a href="' + esc( url ) + '" style="font-size:0.75rem;color:var(--color-link)">Read more \u2192</a>' +
'</div>' +
'</div>';
/* Update the "Read more" span in the card header to link to the article */
var hdrLink = document.getElementById( 'bsw-featured-link' );
if ( hdrLink ) {
/* Replace the span with a proper anchor now that we have a URL */
var a = document.createElement( 'a' );
a.href = url;
a.textContent = 'Read more \u2192';
hdrLink.parentNode.replaceChild( a, hdrLink );
}
}
/**
* Fetch file count from media.battlestarwiki.org and populate
* the #bsw-stat-files span in the statistics card.
*/
function initMediaFileCount() {
var el = document.getElementById( 'bsw-stat-files' );
if ( !el ) return;
apiGetFrom( MEDIA_API, {
action: 'query',
meta: 'siteinfo',
siprop: 'statistics'
} ).then( function ( data ) {
var files = data.query.statistics.images || 0;
el.textContent = files.toLocaleString();
} ).catch( function () {
el.textContent = '—';
} );
}
/**
* Recent changes tab switching.
* Fetches live recent changes from the selected wiki's API
* and replaces the #bsw-rc-content div content.
*/
var rcCurrentWiki = 'all';
var rcCache = {};
function loadRecentChanges( wiki ) {
var wrap = document.getElementById( 'bsw-rc-content' );
if ( !wrap ) return;
if ( rcCache[ wiki ] ) {
wrap.innerHTML = rcCache[ wiki ];
return;
}
/* 'all' shows the cached Special:RecentChanges transclude */
if ( wiki === 'all' ) {
if ( rcCache.all ) {
wrap.innerHTML = rcCache.all;
}
return;
}
wrap.innerHTML = '<p style="color:var(--color-base--subtle);font-size:0.8125rem;padding:0.5rem 0">Loading\u2026</p>';
var apiMap = { en: API, de: DE_API, fr: FR_API, media: MEDIA_API };
var base = apiMap[ wiki ];
if ( !base ) return;
var nsMap = {
en: '0',
de: '0',
fr: '0',
media: '0|6'
};
apiGetFrom( base, {
action: 'query',
list: 'recentchanges',
rcprop: 'title|timestamp|user|sizes',
rcnamespace: nsMap[ wiki ] || '0',
rclimit: '10',
rctype: 'edit|new'
} ).then( function ( data ) {
var changes = ( data.query.recentchanges || [] ).filter( function ( rc ) {
/* Filter out main page and subpages */
return rc.title !== 'Main Page' && rc.title.indexOf( '/' ) === -1;
} );
changes = changes.filter( function ( rc ) {
return rc.title !== 'Main Page' && rc.title.indexOf( '/' ) === -1;
} );
if ( !changes.length ) {
wrap.innerHTML = '<p style="color:var(--color-base--subtle);font-size:0.8125rem;padding:0.5rem 0">No recent changes.</p>';
return;
}
var baseUrl = {
en: 'https://en.battlestarwiki.org/',
de: 'https://de.battlestarwiki.org/',
fr: 'https://fr.battlestarwiki.ddns.net/wiki/',
media: 'https://media.battlestarwiki.org/'
}[ wiki ];
var isCrosswiki = ( wiki !== 'all' && wiki !== 'en' );
/* Deduplicate by title */
var seen = {};
changes = changes.filter( function ( rc ) {
if ( seen[ rc.title ] ) return false;
seen[ rc.title ] = true;
return true;
} );
var html = '<ul class="bsw-recent">' + changes.map( function ( rc ) {
var title = rc.title;
var displayTitle = title.replace( /^[^:]+:/, '' ); /* strip namespace prefix */
var url = baseUrl + encodeURIComponent( title.replace( / /g, '_' ) );
var ts = new Date( rc.timestamp );
var ago = timeAgo( ts );
var cls = isCrosswiki ? ' class="external"' : '';
var target = isCrosswiki ? ' target="_blank"' : '';
return '<li class="ri">' +
'<a href="' + esc( url ) + '"' + cls + target + '>' + esc( displayTitle ) + '</a>' +
'<span class="bsw-recent-time">' + esc( ago ) + '</span>' +
'</li>';
} ).join( '' ) + '</ul>';
rcCache[ wiki ] = html;
wrap.innerHTML = html;
} ).catch( function () {
wrap.innerHTML = '<p style="color:var(--color-base--subtle);font-size:0.8125rem;padding:0.5rem 0">Could not load recent changes.</p>';
} );
}
function timeAgo( date ) {
var diff = Math.floor( ( Date.now() - date.getTime() ) / 1000 );
if ( diff < 60 ) return diff + 's ago';
if ( diff < 3600 ) return Math.floor( diff / 60 ) + ' min ago';
var hrs = Math.floor( diff / 3600 );
if ( diff < 86400 ) return hrs + ( hrs === 1 ? ' hr ago' : ' hrs ago' );
var days = Math.floor( diff / 86400 );
return days + ( days === 1 ? ' day ago' : ' days ago' );
}
/**
* Newest article — pulls from Special:NewPages via API,
* skips subpages, redirects, and disambiguation pages.
*/
function initNewestArticle() {
var inner = document.getElementById( 'bsw-newest-inner' );
if ( !inner ) return;
apiGet( {
action: 'query',
list: 'logevents',
letype: 'create',
lenamespace: '0',
lelimit: '20',
leprop: 'title|timestamp'
} ).then( function ( data ) {
var entries = ( data.query.logevents || [] ).filter( function ( e ) {
return e.title.indexOf( '/' ) === -1 && e.title !== 'Main Page';
} );
if ( !entries.length ) throw new Error( 'No entries' );
var entry = entries[0];
var title = entry.title;
var url = 'https://en.battlestarwiki.org/wiki/' + encodeURIComponent( title.replace( / /g, '_' ) );
var ts = new Date( entry.timestamp );
var date = ts.toLocaleDateString( 'en-US', { year: 'numeric', month: 'long', day: 'numeric' } );
return apiGet( {
action: 'query',
titles: title,
prop: 'extracts|pageimages|pageprops',
exintro: '1',
exchars: '500',
exsectionformat: 'plain',
piprop: 'thumbnail',
pithumbsize: '200',
ppprop: 'disambiguation'
} ).then( function ( d ) {
var page = Object.values( d.query.pages || {} )[0] || {};
/* Skip disambiguation pages, try next */
if ( page.pageprops && page.pageprops.disambiguation !== undefined ) {
var next = entries[1];
if ( !next ) throw new Error( 'No suitable page' );
title = next.title;
url = 'https://en.battlestarwiki.org/wiki/' + encodeURIComponent( title.replace( / /g, '_' ) );
date = new Date( next.timestamp ).toLocaleDateString( 'en-US', { year: 'numeric', month: 'long', day: 'numeric' } );
return apiGet( {
action: 'query', titles: title,
prop: 'extracts|pageimages', exintro: '1', exchars: '300',
exsectionformat: 'plain', piprop: 'thumbnail', pithumbsize: '200'
} ).then( function ( d2 ) {
return { page: Object.values( d2.query.pages || {} )[0] || {}, title: title, url: url, date: date };
} );
}
return { page: page, title: title, url: url, date: date };
} );
} ).then( function ( result ) {
var page = result.page;
var title = result.title || page.title || '';
var url = result.url || ( 'https://en.battlestarwiki.org/wiki/' + encodeURIComponent( ( page.title || '' ).replace( / /g, '_' ) ) );
var thumb = page.thumbnail ? page.thumbnail.source : '';
var extract = page.extract || '';
var thumbHtml = thumb
? '<img src="' + esc( thumb ) + '" alt="' + esc( title ) + '" class="bsw-fa-thumb" width="90" height="110">'
: '<img src="https://en.battlestarwiki.org/resources/BSGWIKILOGO.png" alt="Battlestar Wiki" class="bsw-fa-thumb" width="90" height="110">';
/* Clean up extract — truncate at sentence boundary */
var cleanExtract = extract.replace( /\s*\.\.\.\s*$/, '' );
/* Try to truncate at last sentence end within ~400 chars */
var stripped = cleanExtract.replace( /<[^>]+>/g, '' );
if ( stripped.length > 400 ) {
var lastPeriod = cleanExtract.lastIndexOf( '.', 500 );
if ( lastPeriod > 200 ) {
cleanExtract = cleanExtract.slice( 0, lastPeriod + 1 );
}
}
inner.innerHTML =
thumbHtml +
'<div>' +
'<div class="bsw-fa-title"><a href="' + esc( url ) + '">' + esc( title ) + '</a></div>' +
'<div class="bsw-newest-date">Created ' + esc( result.date ) + '</div>' +
'<div class="bsw-fa-extract">' + cleanExtract + '</div>' +
'<div style="margin-top:0.375rem">' +
'<a href="' + esc( url ) + '" style="font-size:0.75rem;color:var(--color-link)">Read more \u2192</a>' +
'</div>' +
'</div>';
} ).catch( function ( err ) {
inner.innerHTML = '<div style="font-size:0.8125rem;color:var(--color-base--subtle);padding:0.5rem 0">Could not load newest article.</div>';
} );
}
/* ── Entry point ──────────────────────────────────────────────── */
/**
* Hide any .bsw-card whose .bsw-card-body contains nothing but
* a red link (missing page transclusion). This catches subpages
* like BattlestarWiki:Did_you_know, OTD/Month_Day etc. that
* haven't been created yet.
*/
function filterRedlinkCards() {
document.querySelectorAll( '.bsw-main-page .bsw-card' ).forEach( function ( card ) {
/* Skip cards that are JS-populated — they start with a loading spinner
and will be filled in asynchronously */
if ( card.querySelector( '.bsw-loading, .bsw-spinner' ) ) return;
var body = card.querySelector( '.bsw-card-body' );
if ( !body ) return;
/* Remove non-content elements before checking — date labels,
loading spinners etc. shouldn't count as "content" */
var clone = body.cloneNode( true );
/* Remove known non-content wrappers */
clone.querySelectorAll( '.bsw-otd-date, .bsw-spinner, .bsw-loading' )
.forEach( function ( el ) { el.remove(); } );
var text = clone.textContent.trim();
var links = clone.querySelectorAll( 'a' );
var redLinks = clone.querySelectorAll( 'a.new' );
/* Hide if nothing left after stripping non-content */
if ( text === '' ) {
card.style.display = 'none';
return;
}
/* Hide if the only remaining content is red links */
if ( redLinks.length > 0 && redLinks.length === links.length ) {
clone.querySelectorAll( 'a' ).forEach( function ( a ) { a.remove(); } );
if ( clone.textContent.trim() === '' ) {
card.style.display = 'none';
}
}
} );
}
/* bswSetTab — exposed globally as fallback for inline onclick */
window.bswSetTab = function ( el, wiki ) {
document.querySelectorAll( '.bsw-wtab' ).forEach( function ( t ) {
t.classList.remove( 'bsw-active' );
} );
el.classList.add( 'bsw-active' );
rcCurrentWiki = wiki;
loadRecentChanges( wiki );
};
function wireRcTabs() {
var tabMap = { 'All': 'all', 'EN': 'en', 'DE': 'de', 'FR': 'fr', 'Media': 'media' };
document.querySelectorAll( '.bsw-wtab' ).forEach( function ( tab ) {
var wiki = tabMap[ tab.textContent.trim() ];
if ( !wiki ) return;
tab.addEventListener( 'click', function () {
document.querySelectorAll( '.bsw-wtab' ).forEach( function ( t ) {
t.classList.remove( 'bsw-active' );
} );
tab.classList.add( 'bsw-active' );
rcCurrentWiki = wiki;
loadRecentChanges( wiki );
} );
} );
}
mw.hook( 'wikipage.content' ).add( function () {
if ( !document.querySelector( '.bsw-main-page' ) ) return;
initHero();
initFeatured();
initMediaFileCount();
initNewestArticle();
filterRedlinkCards();
wireRcTabs();
/* Cache the initial 'all' RC content so tab switching can restore it */
var rcWrap = document.getElementById( 'bsw-rc-content' );
if ( rcWrap ) rcCache.all = rcWrap.innerHTML;
} );
}() );