UserStats v2: auto-open on tool page, mobile fallback link |
UserStats v3: inline form on tool page, lazy API init |
||
| Line 1: | Line 1: | ||
/* ===================================================================== | /* ===================================================================== | ||
* Bharatpedia Gadget: UserStats | * Bharatpedia Gadget: UserStats (v3) | ||
* | * Interactive user-statistics lookup. On the dedicated tool page | ||
* | * (Bharatpedia:User statistics) the lookup form is embedded directly | ||
* | * in the page content. Everywhere else a "User stats" menu link opens | ||
* | * it as a dialog. Data comes from the wiki's own API (same origin). | ||
* ===================================================================== */ | * ===================================================================== */ | ||
( function () { | ( function () { | ||
'use strict'; | 'use strict'; | ||
var | var TOOL_PAGE = 'Bharatpedia:User_statistics', | ||
DIALOG_ID = 'bp-userstats-dialog', | |||
DIALOG_ID = 'bp-userstats-dialog'; | nsCache = null; | ||
function esc( s ) { | function esc( s ) { | ||
| Line 26: | Line 26: | ||
} | } | ||
function getNamespaces() { | function getNamespaces( api ) { | ||
if ( nsCache ) { | if ( nsCache ) { | ||
return $.Deferred().resolve( nsCache ).promise(); | return $.Deferred().resolve( nsCache ).promise(); | ||
| Line 44: | Line 44: | ||
} | } | ||
function | function fetchContribs( api, username ) { | ||
var all = [], | |||
var | params = { | ||
action: 'query', | |||
list: 'usercontribs', | |||
ucuser: username, | |||
uclimit: 500, | |||
ucprop: 'ids|title|timestamp|sizediff|flags|tags', | |||
formatversion: 2 | |||
}; | |||
function oneBatch() { | |||
return api.get( params ).then( function ( d ) { | |||
all = all.concat( d.query.usercontribs ); | |||
if ( d[ 'continue' ] && all.length < 5000 ) { | |||
params.uccontinue = d[ 'continue' ].uccontinue; | |||
return oneBatch(); | |||
} | |||
return all; | |||
} ); | |||
} | } | ||
return oneBatch(); | |||
} | } | ||
| Line 108: | Line 71: | ||
$out.html( '<p style="color:#72777d">Fetching data…</p>' ); | $out.html( '<p style="color:#72777d">Fetching data…</p>' ); | ||
var userInfo; | var api, userInfo; | ||
try { | |||
api = new mw.Api(); | |||
} catch ( e ) { | |||
$out.html( '<p style="color:#d33">Could not start the lookup. Please reload the page.</p>' ); | |||
$btn.prop( 'disabled', false ).text( 'Check' ); | |||
return; | |||
} | |||
api.get( { | api.get( { | ||
action: 'query', | action: 'query', | ||
| Line 121: | Line 92: | ||
} | } | ||
userInfo = u; | userInfo = u; | ||
return fetchContribs( username ); | return fetchContribs( api, username ); | ||
} ).then( function ( contribs ) { | } ).then( function ( contribs ) { | ||
return getNamespaces().then( function ( nsNames ) { | return getNamespaces( api ).then( function ( nsNames ) { | ||
render( username, userInfo, contribs, nsNames, $out ); | render( username, userInfo, contribs, nsNames, $out ); | ||
} ); | } ); | ||
| Line 132: | Line 103: | ||
$btn.prop( 'disabled', false ).text( 'Check' ); | $btn.prop( 'disabled', false ).text( 'Check' ); | ||
} ); | } ); | ||
} | } | ||
| Line 209: | Line 157: | ||
html += '<table style="width:100%;border-collapse:collapse;font-size:13px">'; | html += '<table style="width:100%;border-collapse:collapse;font-size:13px">'; | ||
nsRows.forEach( function ( id ) { | nsRows.forEach( function ( id ) { | ||
var n = nsCounts[ id ], pct = Math.round( n / analysed * 100 ); | var n = nsCounts[ id ], pct = analysed ? Math.round( n / analysed * 100 ) : 0; | ||
html += '<tr style="border-bottom:1px solid #eaecf0">' + | html += '<tr style="border-bottom:1px solid #eaecf0">' + | ||
'<td style="padding:4px 6px">' + esc( nsNames[ id ] || ( 'NS ' + id ) ) + '</td>' + | '<td style="padding:4px 6px">' + esc( nsNames[ id ] || ( 'NS ' + id ) ) + '</td>' + | ||
| Line 272: | Line 220: | ||
} | } | ||
function buildLookupUI() { | |||
var $wrap = $( '<div>' ); | |||
var $form = $( '<div>' ).css( { display: 'flex', gap: '8px', 'margin-bottom': '14px' } ); | |||
var $input = $( '<input>' ).attr( { type: 'text', placeholder: 'Username — yahan naam likho' } ).css( { | |||
flex: 1, padding: '10px 12px', 'font-size': '15px', | |||
var | border: '2px solid #36c', 'border-radius': '6px' | ||
} ); | |||
var $btn = $( '<button>' ).text( 'Check' ).css( { | |||
padding: '10px 22px', 'font-size': '15px', cursor: 'pointer', | |||
background: '#36c', color: '#fff', border: 'none', 'border-radius': '6px', | |||
'font-weight': 'bold' | |||
} ); | |||
var $out = $( '<div>' ).css( 'font-size', '14px' ); | |||
$form.append( $input, $btn ); | |||
$wrap.append( $form, $out ); | |||
function run() { | |||
var name = $.trim( $input.val() ); | |||
if ( name ) lookup( name, $out, $btn ); | |||
} | } | ||
var $link = mw.util.addPortletLink( | $btn.on( 'click', run ); | ||
$input.on( 'keydown', function ( e ) { if ( e.key === 'Enter' ) run(); } ); | |||
'Look up statistics for any user' | return $wrap; | ||
} | |||
function openDialog() { | |||
closeDialog(); | |||
var $overlay = $( '<div>' ).attr( 'id', DIALOG_ID ).css( { | |||
); | position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, | ||
background: 'rgba(0,0,0,0.55)', 'z-index': 10000, | |||
display: 'flex', 'align-items': 'flex-start', 'justify-content': 'center', | |||
padding: '40px 12px', 'box-sizing': 'border-box', overflow: 'auto' | |||
} ); | |||
var $panel = $( '<div>' ).css( { | |||
background: '#fff', color: '#202122', 'border-radius': '8px', | |||
width: '100%', 'max-width': '640px', padding: '20px 22px', | |||
'box-shadow': '0 8px 40px rgba(0,0,0,0.35)', 'box-sizing': 'border-box' | |||
} ); | |||
var $head = $( '<div>' ).css( { display: 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-bottom': '12px' } ); | |||
$head.append( $( '<b>' ).text( 'User statistics' ).css( 'font-size', '18px' ) ); | |||
var $close = $( '<button>' ).text( '✕' ).css( { | |||
border: '1px solid #a2a9b1', background: '#f8f9fa', 'border-radius': '4px', | |||
cursor: 'pointer', padding: '2px 10px', 'font-size': '14px' | |||
} ).on( 'click', closeDialog ); | |||
$head.append( $close ); | |||
$panel.append( $head, buildLookupUI() ); | |||
$overlay.append( $panel ); | |||
$( document.body ).append( $overlay ); | |||
$overlay.on( 'click', function ( e ) { | |||
if ( e.target === $overlay[ 0 ] ) closeDialog(); | |||
} ); | |||
$( document ).on( 'keydown.bpuserstats', function ( e ) { | |||
if ( e.key === 'Escape' ) closeDialog(); | |||
} ); | |||
} | |||
function closeDialog() { | |||
$( '#' + DIALOG_ID ).remove(); | |||
$( document ).off( 'keydown.bpuserstats' ); | |||
} | |||
function init() { | |||
try { | |||
var pageName = mw.config.get( 'wgPageName' ); | |||
if ( pageName === TOOL_PAGE ) { | |||
var $content = $( '#mw-content-text' ); | |||
if ( !$content.length ) $content = $( '#bodyContent' ); | |||
var $box = $( '<div>' ).attr( 'id', 'bp-userstats-inline' ).css( { | |||
border: '2px solid #36c', 'border-radius': '8px', | |||
padding: '16px', margin: '0 0 18px 0', background: '#f8f9fa' | |||
} ); | |||
$box.append( $( '<h2>' ).text( 'User statistics' ).css( 'margin', '0 0 10px 0' ) ); | |||
$box.append( buildLookupUI() ); | |||
if ( $content.length ) { | |||
$content.prepend( $box ); | |||
} else { | |||
$( document.body ).prepend( $box ); | |||
} | |||
} | |||
var $link = mw.util.addPortletLink( 'p-tb', '#', 'User stats', 't-userstats', 'Look up statistics for any user' ); | |||
if ( !$link ) $link = mw.util.addPortletLink( 'p-cactions', '#', 'User stats', 't-userstats-ca', 'Look up statistics for any user' ); | |||
if ( !$link ) $link = mw.util.addPortletLink( 'p-personal', '#', 'User stats', 't-userstats-personal', 'Look up statistics for any user' ); | |||
if ( $link ) { | |||
$( $link ).on( 'click', function ( e ) { | |||
e.preventDefault(); | |||
openDialog(); | |||
} ); | |||
} | |||
} catch ( e ) { | |||
if ( window.console && console.error ) { | |||
console.error( 'UserStats init failed: ' + ( e && e.message ) ); | |||
} | |||
} | } | ||
} | |||
mw.loader.using( [ 'mediawiki.util', 'mediawiki.api' ], function () { | |||
if ( document.readyState === 'loading' ) { | |||
document.addEventListener( 'DOMContentLoaded', init ); | |||
} else { | |||
init(); | |||
} | } | ||
} ); | } ); | ||
}() ); | }() ); | ||
Revision as of 10:54, 15 October 2026
/* =====================================================================
* Bharatpedia Gadget: UserStats (v3)
* Interactive user-statistics lookup. On the dedicated tool page
* (Bharatpedia:User statistics) the lookup form is embedded directly
* in the page content. Everywhere else a "User stats" menu link opens
* it as a dialog. Data comes from the wiki's own API (same origin).
* ===================================================================== */
( function () {
'use strict';
var TOOL_PAGE = 'Bharatpedia:User_statistics',
DIALOG_ID = 'bp-userstats-dialog',
nsCache = null;
function esc( s ) {
return String( s ).replace( /[&<>"']/g, function ( c ) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ c ];
} );
}
function fmtDate( iso ) {
if ( !iso ) return '—';
var d = new Date( iso );
var months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];
return d.getUTCDate() + ' ' + months[ d.getUTCMonth() ] + ' ' + d.getUTCFullYear();
}
function getNamespaces( api ) {
if ( nsCache ) {
return $.Deferred().resolve( nsCache ).promise();
}
return api.get( {
action: 'query',
meta: 'siteinfo',
siprop: 'namespaces',
formatversion: 2
} ).then( function ( d ) {
nsCache = {};
$.each( d.query.namespaces, function ( id, ns ) {
nsCache[ id ] = ns.canonical || ns.name || ( 'NS ' + id );
} );
return nsCache;
} );
}
function fetchContribs( api, username ) {
var all = [],
params = {
action: 'query',
list: 'usercontribs',
ucuser: username,
uclimit: 500,
ucprop: 'ids|title|timestamp|sizediff|flags|tags',
formatversion: 2
};
function oneBatch() {
return api.get( params ).then( function ( d ) {
all = all.concat( d.query.usercontribs );
if ( d[ 'continue' ] && all.length < 5000 ) {
params.uccontinue = d[ 'continue' ].uccontinue;
return oneBatch();
}
return all;
} );
}
return oneBatch();
}
function lookup( username, $out, $btn ) {
$btn.prop( 'disabled', true ).text( 'Loading…' );
$out.html( '<p style="color:#72777d">Fetching data…</p>' );
var api, userInfo;
try {
api = new mw.Api();
} catch ( e ) {
$out.html( '<p style="color:#d33">Could not start the lookup. Please reload the page.</p>' );
$btn.prop( 'disabled', false ).text( 'Check' );
return;
}
api.get( {
action: 'query',
list: 'users',
ususers: username,
usprop: 'registration|editcount|groups',
formatversion: 2
} ).then( function ( d ) {
var u = d.query.users[ 0 ];
if ( u.missing || u.invalid ) {
throw new Error( 'User "' + username + '" not found.' );
}
userInfo = u;
return fetchContribs( api, username );
} ).then( function ( contribs ) {
return getNamespaces( api ).then( function ( nsNames ) {
render( username, userInfo, contribs, nsNames, $out );
} );
} ).fail( function ( err ) {
var msg = ( err && err.message ) || 'Something went wrong.';
$out.html( '<p style="color:#d33">' + esc( msg ) + '</p>' );
} ).always( function () {
$btn.prop( 'disabled', false ).text( 'Check' );
} );
}
function render( username, u, contribs, nsNames, $out ) {
var total = u.editcount,
nsCounts = {},
mainspace = 0,
minor = 0,
months = {},
pages = {},
tags = {},
i, c, key;
for ( i = 0; i < contribs.length; i++ ) {
c = contribs[ i ];
nsCounts[ c.ns ] = ( nsCounts[ c.ns ] || 0 ) + 1;
if ( c.ns === 0 ) mainspace++;
if ( c.minor ) minor++;
key = c.timestamp.slice( 0, 7 );
months[ key ] = ( months[ key ] || 0 ) + 1;
pages[ c.title ] = ( pages[ c.title ] || 0 ) + 1;
( c.tags || [] ).forEach( function ( t ) {
tags[ t ] = ( tags[ t ] || 0 ) + 1;
} );
}
var analysed = contribs.length,
capped = total > analysed,
mainPct = total ? Math.round( mainspace / total * 100 ) : 0;
var html = '';
html += '<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:14px">';
html += statCard( 'Registered', fmtDate( u.registration ) );
html += statCard( 'Total edits', total.toLocaleString( 'en-IN' ) );
html += statCard( 'Mainspace edits', mainspace.toLocaleString( 'en-IN' ) + ' (' + mainPct + '%)' );
html += statCard( 'Minor edits', minor.toLocaleString( 'en-IN' ) );
html += '</div>';
if ( u.groups && u.groups.length ) {
var g = u.groups.filter( function ( x ) { return x !== '*'; } );
if ( g.length ) {
html += '<p style="margin:0 0 14px"><b>Groups:</b> ' + esc( g.join( ', ' ) ) + '</p>';
}
}
if ( capped ) {
html += '<p style="color:#72777d;margin:0 0 14px">Showing analysis of the latest ' +
analysed.toLocaleString( 'en-IN' ) + ' edits (API cap).</p>';
}
var nsRows = Object.keys( nsCounts ).map( Number ).sort( function ( a, b ) {
return nsCounts[ b ] - nsCounts[ a ];
} );
html += '<h4 style="margin:14px 0 6px">Edits by namespace</h4>';
html += '<table style="width:100%;border-collapse:collapse;font-size:13px">';
nsRows.forEach( function ( id ) {
var n = nsCounts[ id ], pct = analysed ? Math.round( n / analysed * 100 ) : 0;
html += '<tr style="border-bottom:1px solid #eaecf0">' +
'<td style="padding:4px 6px">' + esc( nsNames[ id ] || ( 'NS ' + id ) ) + '</td>' +
'<td style="padding:4px 6px"><div style="background:#eaecf0;border-radius:3px">' +
'<div style="background:#36c;height:8px;border-radius:3px;width:' + pct + '%"></div></div></td>' +
'<td style="padding:4px 6px;text-align:right;white-space:nowrap">' + n.toLocaleString( 'en-IN' ) + '</td></tr>';
} );
html += '</table>';
var mKeys = Object.keys( months ).sort().slice( -12 );
if ( mKeys.length ) {
var max = Math.max.apply( null, mKeys.map( function ( k ) { return months[ k ]; } ) );
html += '<h4 style="margin:14px 0 6px">Monthly activity</h4>';
html += '<div style="display:flex;align-items:flex-end;gap:4px;height:90px">';
mKeys.forEach( function ( k ) {
var h = Math.max( 4, Math.round( months[ k ] / max * 80 ) );
html += '<div title="' + esc( k ) + ': ' + months[ k ] + ' edits" style="flex:1;background:#36c;border-radius:3px 3px 0 0;height:' + h + 'px"></div>';
} );
html += '</div>';
html += '<div style="display:flex;gap:4px;font-size:10px;color:#72777d">';
mKeys.forEach( function ( k ) {
html += '<div style="flex:1;text-align:center;overflow:hidden">' + esc( k.slice( 2 ) ) + '</div>';
} );
html += '</div>';
}
var tagKeys = Object.keys( tags ).sort( function ( a, b ) { return tags[ b ] - tags[ a ]; } ).slice( 0, 8 );
if ( tagKeys.length ) {
html += '<h4 style="margin:14px 0 6px">Edit tags</h4><p style="margin:0;font-size:13px">' +
tagKeys.map( function ( t ) { return esc( t ) + ' (' + tags[ t ] + ')'; } ).join( ' · ' ) + '</p>';
}
var topPages = Object.keys( pages ).sort( function ( a, b ) { return pages[ b ] - pages[ a ]; } ).slice( 0, 8 );
if ( topPages.length ) {
html += '<h4 style="margin:14px 0 6px">Most edited pages</h4><ul style="margin:0;padding-left:20px;font-size:13px">';
topPages.forEach( function ( t ) {
html += '<li>' + esc( t ) + ' <span style="color:#72777d">(' + pages[ t ] + ')</span></li>';
} );
html += '</ul>';
}
html += '<h4 style="margin:14px 0 6px">Recent edits</h4><ul style="margin:0;padding-left:20px;font-size:13px">';
contribs.slice( 0, 10 ).forEach( function ( ed ) {
html += '<li>' + esc( ed.title ) +
' <span style="color:#72777d">' + fmtDate( ed.timestamp ) + '</span></li>';
} );
html += '</ul>';
html += '<p style="margin:14px 0 0;font-size:12px;color:#72777d">Profile: <a href="' +
mw.util.getUrl( 'Special:Contributions/' + username ) + '">contributions</a> · ' +
'<a href="' + mw.util.getUrl( 'User:' + username ) + '">user page</a></p>';
$out.html( html );
}
function statCard( label, value ) {
return '<div style="flex:1;min-width:120px;background:#f8f9fa;border:1px solid #eaecf0;' +
'border-radius:6px;padding:10px 12px">' +
'<div style="font-size:11px;color:#72777d;text-transform:uppercase;letter-spacing:0.5px">' +
esc( label ) + '</div>' +
'<div style="font-size:18px;font-weight:bold;margin-top:2px">' + esc( value ) + '</div></div>';
}
function buildLookupUI() {
var $wrap = $( '<div>' );
var $form = $( '<div>' ).css( { display: 'flex', gap: '8px', 'margin-bottom': '14px' } );
var $input = $( '<input>' ).attr( { type: 'text', placeholder: 'Username — yahan naam likho' } ).css( {
flex: 1, padding: '10px 12px', 'font-size': '15px',
border: '2px solid #36c', 'border-radius': '6px'
} );
var $btn = $( '<button>' ).text( 'Check' ).css( {
padding: '10px 22px', 'font-size': '15px', cursor: 'pointer',
background: '#36c', color: '#fff', border: 'none', 'border-radius': '6px',
'font-weight': 'bold'
} );
var $out = $( '<div>' ).css( 'font-size', '14px' );
$form.append( $input, $btn );
$wrap.append( $form, $out );
function run() {
var name = $.trim( $input.val() );
if ( name ) lookup( name, $out, $btn );
}
$btn.on( 'click', run );
$input.on( 'keydown', function ( e ) { if ( e.key === 'Enter' ) run(); } );
return $wrap;
}
function openDialog() {
closeDialog();
var $overlay = $( '<div>' ).attr( 'id', DIALOG_ID ).css( {
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
background: 'rgba(0,0,0,0.55)', 'z-index': 10000,
display: 'flex', 'align-items': 'flex-start', 'justify-content': 'center',
padding: '40px 12px', 'box-sizing': 'border-box', overflow: 'auto'
} );
var $panel = $( '<div>' ).css( {
background: '#fff', color: '#202122', 'border-radius': '8px',
width: '100%', 'max-width': '640px', padding: '20px 22px',
'box-shadow': '0 8px 40px rgba(0,0,0,0.35)', 'box-sizing': 'border-box'
} );
var $head = $( '<div>' ).css( { display: 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-bottom': '12px' } );
$head.append( $( '<b>' ).text( 'User statistics' ).css( 'font-size', '18px' ) );
var $close = $( '<button>' ).text( '✕' ).css( {
border: '1px solid #a2a9b1', background: '#f8f9fa', 'border-radius': '4px',
cursor: 'pointer', padding: '2px 10px', 'font-size': '14px'
} ).on( 'click', closeDialog );
$head.append( $close );
$panel.append( $head, buildLookupUI() );
$overlay.append( $panel );
$( document.body ).append( $overlay );
$overlay.on( 'click', function ( e ) {
if ( e.target === $overlay[ 0 ] ) closeDialog();
} );
$( document ).on( 'keydown.bpuserstats', function ( e ) {
if ( e.key === 'Escape' ) closeDialog();
} );
}
function closeDialog() {
$( '#' + DIALOG_ID ).remove();
$( document ).off( 'keydown.bpuserstats' );
}
function init() {
try {
var pageName = mw.config.get( 'wgPageName' );
if ( pageName === TOOL_PAGE ) {
var $content = $( '#mw-content-text' );
if ( !$content.length ) $content = $( '#bodyContent' );
var $box = $( '<div>' ).attr( 'id', 'bp-userstats-inline' ).css( {
border: '2px solid #36c', 'border-radius': '8px',
padding: '16px', margin: '0 0 18px 0', background: '#f8f9fa'
} );
$box.append( $( '<h2>' ).text( 'User statistics' ).css( 'margin', '0 0 10px 0' ) );
$box.append( buildLookupUI() );
if ( $content.length ) {
$content.prepend( $box );
} else {
$( document.body ).prepend( $box );
}
}
var $link = mw.util.addPortletLink( 'p-tb', '#', 'User stats', 't-userstats', 'Look up statistics for any user' );
if ( !$link ) $link = mw.util.addPortletLink( 'p-cactions', '#', 'User stats', 't-userstats-ca', 'Look up statistics for any user' );
if ( !$link ) $link = mw.util.addPortletLink( 'p-personal', '#', 'User stats', 't-userstats-personal', 'Look up statistics for any user' );
if ( $link ) {
$( $link ).on( 'click', function ( e ) {
e.preventDefault();
openDialog();
} );
}
} catch ( e ) {
if ( window.console && console.error ) {
console.error( 'UserStats init failed: ' + ( e && e.message ) );
}
}
}
mw.loader.using( [ 'mediawiki.util', 'mediawiki.api' ], function () {
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', init );
} else {
init();
}
} );
}() );