The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* =====================================================================
 * Bharatpedia Gadget: UserStats (v9.3)
 * Interactive user-statistics dashboard. On the dedicated tool page
 * (Bharatpedia:User statistics) the wikitext contains a static search
 * box (#bp-us-static) so it is visible immediately on page load; the
 * gadget swaps it for the full interactive dashboard (stat cards and
 * tabbed views: Overview / Activity / Pages created / Recent). Opening
 * a subpage like Bharatpedia:User_statistics/username auto-runs the
 * lookup for that username. On Special:Contributions pages a "User
 * stats" link is added to the footer, opening the stats page for the
 * viewed user. Everywhere else a "User stats" menu link opens the same
 * dashboard as a dialog. Stats include: registration, edit counts,
 * edits/day, bytes added/removed, first edit, block history, reverted
 * edits, namespace breakdown, edit tags, monthly activity, hourly
 * heatmap, pages created, and pages deleted by the user. All
 * user-facing text is in English. 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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ 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 fetchBlocks( api, username ) {
		return api.get( {
			action: 'query',
			list: 'logevents',
			letype: 'block',
			letitle: 'User:' + username,
			lelimit: 50,
			formatversion: 2
		} ).then( function ( d ) {
			return d.query.logevents;
		} ).fail( function () {
			return [];
		} );
	}

	// Fetch pages created by the user (only existing ones via ucshow=new)
	function fetchCreatedPages( api, username ) {
		var all = [],
			params = {
				action: 'query',
				list: 'usercontribs',
				ucuser: username,
				ucshow: 'new',
				uclimit: 500,
				ucprop: 'ids|title|timestamp',
				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().then( null, function () {
			return [];
		} );
	}

	// Fetch pages deleted by the user (from the deletion log)
	function fetchDeletedPages( api, username ) {
		return api.get( {
			action: 'query',
			list: 'logevents',
			letype: 'delete',
			leuser: username,
			lelimit: 50,
			formatversion: 2
		} ).then( function ( d ) {
			return d.query.logevents;
		} ).then( null, function () {
			return [];
		} );
	}

	// Fetch everything for a username. Resolves { user, contribs, nsNames, blocks, createdPages, deletedPages, stats }.
	function lookupData( username ) {
		var api;
		try {
			api = new mw.Api();
		} catch ( e ) {
			return $.Deferred().reject( new Error( 'Could not start the lookup. Please reload the page.' ) ).promise();
		}
		var userInfo;
		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 $.when(
				fetchContribs( api, username ),
				getNamespaces( api ),
				fetchBlocks( api, username ),
				fetchCreatedPages( api, username ),
				fetchDeletedPages( api, username )
			);
		} ).then( function ( contribs, nsNames, blocks, createdPages, deletedPages ) {
			return {
				user: userInfo,
				contribs: contribs,
				nsNames: nsNames,
				blocks: blocks,
				createdPages: createdPages,
				deletedPages: deletedPages,
				stats: analyse( contribs, userInfo )
			};
		} );
	}

	function analyse( contribs, user ) {
		var nsCounts = {}, mainspace = 0, minor = 0,
			months = {}, tags = {}, hours = {},
			reverted = 0, bytesAdded = 0, bytesRemoved = 0,
			i, c, key, sd;
		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;
			( c.tags || [] ).forEach( function ( t ) {
				tags[ t ] = ( tags[ t ] || 0 ) + 1;
			} );
			if ( ( c.tags || [] ).indexOf( 'mw-reverted' ) !== -1 ) reverted++;
			sd = c.sizediff || 0;
			if ( sd > 0 ) bytesAdded += sd; else bytesRemoved -= sd;
			key = new Date( c.timestamp ).getUTCHours();
			hours[ key ] = ( hours[ key ] || 0 ) + 1;
		}
		var firstEdit = contribs.length ? contribs[ contribs.length - 1 ].timestamp : null;
		var daysOld = user && user.registration ?
			Math.max( 1, Math.floor( ( Date.now() - new Date( user.registration ).getTime() ) / 864e5 ) ) : 0;
		var editsPerDay = daysOld ? ( user.editcount / daysOld ) : 0;
		return {
			nsCounts: nsCounts, mainspace: mainspace, minor: minor, months: months,
			tags: tags, hours: hours, reverted: reverted,
			bytesAdded: bytesAdded, bytesRemoved: bytesRemoved,
			firstEdit: firstEdit, editsPerDay: editsPerDay
		};
	}

	function statCard( label, value, sub ) {
		var $c = $( '<div>' ).css( {
			flex: '1 1 140px', background: '#fff', border: '1px solid #c8ccd1',
			'border-radius': '6px', padding: '12px 14px', 'box-sizing': 'border-box'
		} );
		$c.append( $( '<div>' ).text( label ).css( {
			'font-size': '11px', color: '#72777d', 'text-transform': 'uppercase',
			'letter-spacing': '0.8px', 'margin-bottom': '4px'
		} ) );
		$c.append( $( '<div>' ).text( value ).css( { 'font-size': '30px', 'font-weight': 'bold', color: '#202122', 'line-height': '1.1' } ) );
		if ( sub ) {
			$c.append( $( '<div>' ).text( sub ).css( { 'font-size': '12px', color: '#72777d', 'margin-top': '4px' } ) );
		}
		return $c;
	}

	function sectionTitle( t ) {
		return $( '<h4>' ).text( t ).css( { margin: '16px 0 8px', 'font-size': '14px', color: '#202122' } );
	}

	function renderDashboard( $root, username, data ) {
		var u = data.user, contribs = data.contribs, nsNames = data.nsNames, st = data.stats,
			total = u.editcount, analysed = contribs.length,
			mainPct = total ? Math.round( st.mainspace / total * 100 ) : 0,
			hidden = total - analysed;

		$root.find( '.bp-us-name' ).text( username );
		$root.find( '.bp-us-results' ).show();

		var $cards = $root.find( '.bp-us-cards' ).empty();
		$cards.append( statCard( 'Registered', fmtDate( u.registration ), u.registration ? Math.floor( ( Date.now() - new Date( u.registration ).getTime() ) / 864e5 ) + ' days old' : '' ) );
		$cards.append( statCard( 'Total edits', Number( total ).toLocaleString( 'en-IN' ), 'Account counter' ) );
		$cards.append( statCard( 'Mainspace', Number( st.mainspace ).toLocaleString( 'en-IN' ), mainPct + '% of visible edits' ) );
		$cards.append( statCard( 'Minor edits', Number( st.minor ).toLocaleString( 'en-IN' ), analysed ? Math.round( st.minor / analysed * 100 ) + '% of analysed' : '' ) );
		$cards.append( statCard( 'Edits / day', st.editsPerDay >= 10 ? Math.round( st.editsPerDay ).toLocaleString( 'en-IN' ) : st.editsPerDay.toFixed( 1 ), 'Activity rate' ) );
		$cards.append( statCard( 'Bytes added', '+' + Number( st.bytesAdded ).toLocaleString( 'en-IN' ), '−' + Number( st.bytesRemoved ).toLocaleString( 'en-IN' ) + ' removed' ) );

		if ( hidden > 0 ) {
			$cards.append( $( '<div>' ).css( {
				'flex-basis': '100%', background: '#eaf3ff', border: '1px solid #c8ccd1',
				'border-radius': '6px', padding: '10px 14px', 'font-size': '13px', color: '#202122'
			} ).html( '<b>' + hidden.toLocaleString( 'en-IN' ) + ' edits are not in the public contribution list.</b><br>The account counter can include revisions that were deleted or hidden.' ) );
		}

		var groups = ( u.groups || [] ).filter( function ( x ) { return x !== '*'; } );

		var $ov = $root.find( '.bp-us-pane-overview' ).empty();
		if ( groups.length ) {
			$ov.append( $( '<p>' ).css( { margin: '0 0 6px', 'font-size': '13px' } )
				.html( '<b>Groups:</b> ' + esc( groups.join( ', ' ) ) ) );
		}
		$ov.append( $( '<p>' ).css( { margin: '0 0 6px', 'font-size': '13px' } )
			.html( '<b>First edit:</b> ' + esc( fmtDate( st.firstEdit ) ) ) );
		if ( st.reverted > 0 ) {
			$ov.append( $( '<p>' ).css( { margin: '0 0 6px', 'font-size': '13px', color: '#d33' } )
				.html( '<b>Reverted edits:</b> ' + st.reverted.toLocaleString( 'en-IN' ) ) );
		}
		var blocks = data.blocks || [];
		if ( blocks.length ) {
			$ov.append( sectionTitle( 'Block history (' + blocks.length + ')' ) );
			var $bl = $( '<ul>' ).css( { margin: '0', 'padding-left': '20px', 'font-size': '13px' } );
			blocks.slice( 0, 5 ).forEach( function ( b ) {
				var action = b.type === 'block' ? ( b.action === 'unblock' ? 'Unblocked' : 'Blocked' ) : b.action;
				$bl.append( $( '<li>' ).html(
					'<b>' + esc( action ) + '</b> ' + esc( fmtDate( b.timestamp ) ) +
					( b.comment ? ' — <span style="color:#54595d">' + esc( b.comment ) + '</span>' : '' )
				) );
			} );
			$ov.append( $bl );
		} else {
			$ov.append( $( '<p>' ).css( { margin: '0 0 6px', 'font-size': '13px', color: '#14866d' } )
				.html( '<b>Block history:</b> clean — never blocked' ) );
		}
		$ov.append( sectionTitle( 'Edits by namespace' ) );
		var $tbl = $( '<table>' ).css( { width: '100%', 'border-collapse': 'collapse', 'font-size': '13px' } );
		Object.keys( st.nsCounts ).map( Number ).sort( function ( a, b ) { return st.nsCounts[ b ] - st.nsCounts[ a ]; } )
			.forEach( function ( id ) {
				var n = st.nsCounts[ id ], pct = analysed ? Math.round( n / analysed * 100 ) : 0;
				var $tr = $( '<tr>' ).css( { 'border-bottom': '1px solid #eaecf0' } );
				$tr.append( $( '<td>' ).text( nsNames[ id ] || ( 'NS ' + id ) ).css( { padding: '5px 6px' } ) );
				var $bar = $( '<div>' ).css( { background: '#eaecf0', 'border-radius': '3px' } );
				$bar.append( $( '<div>' ).css( { background: '#36c', height: '8px', 'border-radius': '3px', width: pct + '%' } ) );
				$tr.append( $( '<td>' ).append( $bar ).css( { padding: '5px 6px' } ) );
				$tr.append( $( '<td>' ).text( n.toLocaleString( 'en-IN' ) ).css( { padding: '5px 6px', 'text-align': 'right', 'white-space': 'nowrap' } ) );
				$tbl.append( $tr );
			} );
		$ov.append( $tbl );

		var tagKeys = Object.keys( st.tags ).sort( function ( a, b ) { return st.tags[ b ] - st.tags[ a ]; } ).slice( 0, 8 );
		if ( tagKeys.length ) {
			$ov.append( sectionTitle( 'Edit tags' ) );
			$ov.append( $( '<p>' ).css( { margin: '0', 'font-size': '13px' } )
				.text( tagKeys.map( function ( t ) { return t + ' (' + st.tags[ t ] + ')'; } ).join( ' · ' ) ) );
		}

		var $ac = $root.find( '.bp-us-pane-activity' ).empty();
		var mKeys = Object.keys( st.months ).sort().slice( -12 );
		if ( mKeys.length ) {
			$ac.append( sectionTitle( 'Monthly activity (last 12 months)' ) );
			var max = Math.max.apply( null, mKeys.map( function ( k ) { return st.months[ k ]; } ) );
			var $bars = $( '<div>' ).css( { display: 'flex', 'align-items': 'flex-end', gap: '5px', height: '110px', 'margin-top': '8px' } );
			mKeys.forEach( function ( k ) {
				var h = Math.max( 5, Math.round( st.months[ k ] / max * 95 ) );
				$bars.append( $( '<div>' ).attr( 'title', k + ': ' + st.months[ k ] + ' edits' ).css( {
					flex: '1', background: '#36c', 'border-radius': '4px 4px 0 0', height: h + 'px', cursor: 'default'
				} ) );
			} );
			$ac.append( $bars );
			var $lbl = $( '<div>' ).css( { display: 'flex', gap: '5px', 'font-size': '10px', color: '#72777d', 'margin-top': '4px' } );
			mKeys.forEach( function ( k ) {
				$lbl.append( $( '<div>' ).text( k.slice( 2 ) + ' (' + st.months[ k ] + ')' ).css( { flex: '1', 'text-align': 'center', overflow: 'hidden' } ) );
			} );
			$ac.append( $lbl );
		} else {
			$ac.append( $( '<p>' ).css( { color: '#72777d' } ).text( 'No dated edits found.' ) );
		}

		$ac.append( sectionTitle( 'Editing hours (UTC)' ) );
		var hMax = 0, h;
		for ( h = 0; h < 24; h++ ) {
			hMax = Math.max( hMax, st.hours[ h ] || 0 );
		}
		var $hbars = $( '<div>' ).css( { display: 'flex', 'align-items': 'flex-end', gap: '3px', height: '80px', 'margin-top': '8px' } );
		for ( h = 0; h < 24; h++ ) {
			var hn = st.hours[ h ] || 0;
			var hh = hMax ? Math.max( 4, Math.round( hn / hMax * 70 ) ) : 4;
			$hbars.append( $( '<div>' ).attr( 'title', h + ':00 UTC — ' + hn + ' edits' ).css( {
				flex: '1', background: hn ? '#0a6b4f' : '#eaecf0', 'border-radius': '3px 3px 0 0',
				height: hh + 'px', cursor: 'default'
			} ) );
		}
		$ac.append( $hbars );
		var $hlbl = $( '<div>' ).css( { display: 'flex', gap: '3px', 'font-size': '9px', color: '#72777d', 'margin-top': '4px' } );
		for ( h = 0; h < 24; h++ ) {
			$hlbl.append( $( '<div>' ).text( h % 6 === 0 ? h + 'h' : '' ).css( { flex: '1', 'text-align': 'center' } ) );
		}
		$ac.append( $hlbl );

		// Pages created pane
		var $cr = $root.find( '.bp-us-pane-created' ).empty();
		var created = data.createdPages || [];
		if ( created.length ) {
			$cr.append( sectionTitle( 'Pages created (' + created.length + ')' ) );
			var byNs = {};
			created.forEach( function ( c ) {
				var ns = c.ns;
				if ( !byNs[ ns ] ) byNs[ ns ] = [];
				byNs[ ns ].push( c );
			} );
			Object.keys( byNs ).map( Number ).sort( function ( a, b ) {
				return byNs[ b ].length - byNs[ a ].length;
			} ).forEach( function ( ns ) {
				var pages = byNs[ ns ];
				var nsName = nsNames[ ns ] || ( 'NS ' + ns );
				$cr.append( $( '<h5>' ).text( nsName + ' (' + pages.length + ')' ).css( { margin: '12px 0 4px', 'font-size': '13px', color: '#202122' } ) );
				var $ul = $( '<ul>' ).css( { margin: '0', 'padding-left': '20px', 'font-size': '13px', 'max-height': '150px', 'overflow': 'auto' } );
				pages.slice( 0, 100 ).forEach( function ( p ) {
					$ul.append( $( '<li>' ).html( esc( p.title ) + ' <span style="color:#72777d">' + fmtDate( p.timestamp ) + '</span>' ) );
				} );
				if ( pages.length > 100 ) {
					$ul.append( $( '<li>' ).text( '... and ' + ( pages.length - 100 ) + ' more' ).css( 'color', '#72777d' ) );
				}
				$cr.append( $ul );
			} );
		} else {
			$cr.append( $( '<p>' ).css( { color: '#72777d' } ).text( 'No pages created by this user found.' ) );
		}

		var deleted = data.deletedPages || [];
		if ( deleted.length ) {
			$cr.append( sectionTitle( 'Pages deleted by this user (' + deleted.length + ')' ) );
			var $dul = $( '<ul>' ).css( { margin: '0', 'padding-left': '20px', 'font-size': '13px', 'max-height': '150px', 'overflow': 'auto' } );
			deleted.forEach( function ( d ) {
				$dul.append( $( '<li>' ).html(
					'<b>' + esc( d.title ) + '</b> ' + esc( fmtDate( d.timestamp ) ) +
					( d.comment ? ' — <span style="color:#54595d">' + esc( d.comment ) + '</span>' : '' ) +
					' (by ' + esc( d.user ) + ')'
				) );
			} );
			$cr.append( $dul );
		} else {
			$cr.append( $( '<p>' ).css( { 'font-size': '13px', color: '#72777d' } ).text( 'No pages deleted by this user found in the deletion log.' ) );
		}

		var $rc = $root.find( '.bp-us-pane-recent' ).empty();
		$rc.append( sectionTitle( 'Recent edits' ) );
		var $rul = $( '<ul>' ).css( { margin: '0', 'padding-left': '20px', 'font-size': '13px' } );
		contribs.slice( 0, 20 ).forEach( function ( ed ) {
			$rul.append( $( '<li>' ).html( esc( ed.title ) + ' <span style="color:#72777d">' + fmtDate( ed.timestamp ) + '</span>' ) );
		} );
		$rc.append( $rul );
		$rc.append( $( '<p>' ).css( { 'font-size': '12px', color: '#72777d', 'margin-top': '12px' } ).html(
			'Profile: <a href="' + mw.util.getUrl( 'Special:Contributions/' + username ) + '">contributions</a> · ' +
			'<a href="' + mw.util.getUrl( 'User:' + username ) + '">user page</a>' ) );

		$root.find( '.bp-us-tab' ).removeClass( 'bp-us-active' );
		$root.find( '.bp-us-tab[data-pane="overview"]' ).addClass( 'bp-us-active' );
		$root.find( '.bp-us-pane' ).hide();
		$root.find( '.bp-us-pane-overview' ).show();
	}

	function buildDashboard( autoUsername ) {
		var $root = $( '<div>' ).addClass( 'bp-userstats-dash' ).css( {
			border: '2px solid #36c', 'border-radius': '10px', padding: '18px',
			margin: '0 0 18px 0', background: '#f8f9fa', 'box-sizing': 'border-box'
		} );

		$root.append( $( '<h2>' ).text( 'User statistics' ).css( { margin: '0 0 4px 0', 'font-size': '22px' } ) );
		$root.append( $( '<p>' ).text( 'Look up registration date, edit count, mainspace edits and activity for any username.' )
			.css( { margin: '0 0 12px 0', 'font-size': '13px', color: '#54595d' } ) );

		var $form = $( '<div>' ).css( { display: 'flex', gap: '8px', 'margin-bottom': '6px' } );
		var $input = $( '<input>' ).attr( { type: 'text', placeholder: 'Enter a username' } ).css( {
			flex: '1', padding: '10px 12px', 'font-size': '15px',
			border: '2px solid #36c', 'border-radius': '6px', 'box-sizing': 'border-box'
		} );
		var $btn = $( '<button>' ).text( 'Check' ).css( {
			padding: '10px 24px', 'font-size': '15px', cursor: 'pointer',
			background: '#36c', color: '#fff', border: 'none', 'border-radius': '6px', 'font-weight': 'bold'
		} );
		$form.append( $input, $btn );
		$root.append( $form );

		var $msg = $( '<div>' ).addClass( 'bp-us-msg' ).css( { 'font-size': '13px', color: '#d33', 'margin-bottom': '6px' } ).hide();
		$root.append( $msg );

		var $results = $( '<div>' ).addClass( 'bp-us-results' ).hide();
		$results.append( $( '<h3>' ).addClass( 'bp-us-name' ).css( { margin: '14px 0 10px 0', 'font-size': '26px' } ) );
		$results.append( $( '<div>' ).addClass( 'bp-us-cards' ).css( { display: 'flex', gap: '10px', 'flex-wrap': 'wrap', 'margin-bottom': '6px' } ) );

		var $tabbar = $( '<div>' ).css( { display: 'flex', gap: '0', 'margin-top': '14px', 'border-bottom': '2px solid #c8ccd1' } );
		var panes = {};
		[ [ 'overview', 'Overview' ], [ 'activity', 'Activity' ], [ 'created', 'Pages created' ], [ 'recent', 'Recent' ] ].forEach( function ( p ) {
			var key = p[ 0 ], label = p[ 1 ];
			var $tab = $( '<button>' ).addClass( 'bp-us-tab' ).attr( 'data-pane', key ).text( label ).css( {
				padding: '8px 18px', 'font-size': '14px', cursor: 'pointer', background: 'transparent',
				border: 'none', 'border-bottom': '3px solid transparent', 'margin-bottom': '-2px', color: '#54595d'
			} );
			$tab.on( 'click', function () {
				$root.find( '.bp-us-tab' ).removeClass( 'bp-us-active' ).css( { 'border-bottom-color': 'transparent', color: '#54595d', 'font-weight': 'normal' } );
				$tab.addClass( 'bp-us-active' ).css( { 'border-bottom-color': '#36c', color: '#202122', 'font-weight': 'bold' } );
				$root.find( '.bp-us-pane' ).hide();
				panes[ key ].show();
			} );
			$tabbar.append( $tab );
			panes[ key ] = $( '<div>' ).addClass( 'bp-us-pane bp-us-pane-' + key ).hide();
			$results.append( panes[ key ] );
		} );
		$results.prepend( $tabbar );

		$root.append( $results );

		function run() {
			var name = $.trim( $input.val() );
			if ( !name ) return;
			$msg.hide();
			$btn.prop( 'disabled', true ).text( 'Loading…' );
			lookupData( name ).then( function ( data ) {
				renderDashboard( $root, name, data );
			} ).fail( function ( err ) {
				$msg.text( ( err && err.message ) || 'Something went wrong.' ).show();
				$results.hide();
			} ).always( function () {
				$btn.prop( 'disabled', false ).text( 'Check' );
			} );
		}
		$btn.on( 'click', run );
		$input.on( 'keydown', function ( e ) { if ( e.key === 'Enter' ) run(); } );

		if ( autoUsername ) {
			$input.val( autoUsername );
			run();
		}

		return $root;
	}

	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: '30px 12px', 'box-sizing': 'border-box', overflow: 'auto'
		} );
		var $panel = $( '<div>' ).css( {
			background: '#fff', color: '#202122', 'border-radius': '10px',
			width: '100%', 'max-width': '680px', padding: '6px',
			'box-shadow': '0 8px 40px rgba(0,0,0,0.35)', 'box-sizing': 'border-box', position: 'relative'
		} );
		var $close = $( '<button>' ).text( '✕' ).css( {
			position: 'absolute', top: '10px', right: '10px', 'z-index': '1',
			border: '1px solid #a2a9b1', background: '#f8f9fa', 'border-radius': '4px',
			cursor: 'pointer', padding: '2px 10px', 'font-size': '14px'
		} ).on( 'click', closeDialog );
		$panel.append( $close, buildDashboard() );
		$panel.find( '.bp-userstats-dash' ).css( { border: 'none', margin: '0' } );
		$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 addContribFooterLink() {
		try {
			var pageName = mw.config.get( 'wgPageName' ) || '',
				isContrib = mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Contributions' ||
					pageName.indexOf( 'Special:Contributions' ) === 0;
			if ( !isContrib ) return;
			var user = mw.config.get( 'wgRelevantUserName' );
			if ( !user ) return;
			var url = mw.util.getUrl( 'Bharatpedia:User statistics/' + user );
			var footerTexts = [ 'Subpages', 'User rights', 'Edit count', 'Articles created', 'accounts(meta)' ];

			function findAnchor() {
				var $found = null;
				$( '#mw-content-text a, main a' ).each( function () {
					if ( footerTexts.indexOf( $.trim( $( this ).text() ) ) !== -1 ) {
						$found = $( this );
					}
				} );
				return $found;
			}

			function add() {
				if ( $( '.bp-us-footer-link' ).length ) return true;
				var $anchor = findAnchor();
				if ( !$anchor ) return false;
				var $link = $( '<a>' ).attr( 'href', url ).addClass( 'bp-us-footer-link' ).text( 'User stats' );
				$anchor.after( document.createTextNode( '\u00a0\u00b7\u00a0' ), $link );
				return true;
			}

			if ( !add() ) {
				var tries = 0;
				var timer = setInterval( function () {
					if ( add() || ++tries >= 10 ) {
						clearInterval( timer );
					}
				}, 500 );
			}
		} catch ( e ) {
			if ( window.console && console.error ) {
				console.error( 'UserStats footer link failed: ' + ( e && e.message ) );
			}
		}
	}

	function init() {
		try {
			var pageName = mw.config.get( 'wgPageName' );

			var onToolPage = pageName === TOOL_PAGE,
			subUser = null;
			if ( !onToolPage && pageName.indexOf( TOOL_PAGE + '/' ) === 0 ) {
				subUser = pageName.substring( TOOL_PAGE.length + 1 ).replace( /_/g, ' ' );
				onToolPage = true;
			}

			if ( onToolPage ) {
				var $static = $( '#bp-us-static' );
				var $dash = buildDashboard( subUser );
				if ( $static.length ) {
					$static.replaceWith( $dash );
				} else {
					var $content = $( '#mw-content-text' );
					if ( !$content.length ) $content = $( '#bodyContent' );
					if ( $content.length ) {
						$content.prepend( $dash );
					} else {
						$( document.body ).prepend( $dash );
					}
				}
			}

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

			addContribFooterLink();
		} catch ( e ) {
			if ( window.console && console.error ) {
				console.error( 'UserStats init failed: ' + ( e && e.message ) );
			}
		}
	}

	try {
		if ( document.readyState === 'loading' ) {
			document.addEventListener( 'DOMContentLoaded', init );
		} else {
			init();
		}
	} catch ( e ) {
		if ( window.console && console.error ) {
			console.error( 'UserStats bootstrap failed: ' + ( e && e.message ) );
		}
	}
}() );