Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.
       🚧 True to our name, we’re still a work in progress. 🚧
   
       You’re welcome to explore, but account registration is currently invite-only as we finalize the setup. 
       Join our forum or follow Mastodon for updates. 
       Full Wiki launch coming soon!
   

MediaWiki:Gadget-projects.js

MediaWiki interface page
Revision as of 05:06, 14 March 2026 by Anthony (talk | contribs) (Removed protection from "MediaWiki:Gadget-projects.js")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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.
/* =============================================================================
   Gadget-projects.js
   Handles: tag rendering, filter bar, contributor count + last edited.
   ============================================================================= */
( function () {
    var pageName = mw.config.get( 'wgPageName' ) || '';
    if ( !/^Projects[:/][^/]+$/.test( pageName ) ) return;

    $( function () {
        var $grid      = $( '.project-hub-grid' );
        var $filterBar = $( '.project-hub-filters' );
        if ( !$grid.length || !$filterBar.length ) return;

        var $cards   = $grid.find( '.project-card-wrap' );
        var total    = $cards.length;
        var hubSlug  = pageName.replace( /^Projects[:/]/, '' );

        // --- Tag spans -------------------------------------------------------
        $( '.project-card-tags[data-tags]' ).each( function () {
            var $el = $( this );
            $el.attr( 'data-tags' ).trim().split( /[\s,]+/ ).forEach( function ( tag ) {
                if ( tag ) $el.append( $( '<span>' ).addClass( 'project-card-tag' ).text( tag ) );
            } );
        } );

        // --- Filter bar (options populated from Cargo) -----------------------
        var $search  = $( '<input>' ).attr( { type: 'text', placeholder: 'Search projects, tags, creators...', 'class': 'project-hub-search' } );
        var $status  = $( '<select>' ).addClass( 'project-hub-select' ).append( $( '<option>' ).val( '' ).text( 'All statuses' ) );
        var $license = $( '<select>' ).addClass( 'project-hub-select' ).append( $( '<option>' ).val( '' ).text( 'All licenses' ) );
        var $sort    = $( '<select>' ).addClass( 'project-hub-select' ).append(
            $( '<option>' ).val( 'alpha' ).text( 'A – Z' ),
            $( '<option>' ).val( 'edited' ).text( 'Recently edited' ),
            $( '<option>' ).val( 'contributors' ).text( 'Most contributors' )
        );
        var $count = $( '<span>' ).addClass( 'project-hub-count' )
            .text( total + ' project' + ( total === 1 ? '' : 's' ) );

        $filterBar.append( $search, $status, $license, $sort, $count );

        // Populate status + license options from Cargo
        var apiBase = mw.util.wikiScript( 'api' );

        $.getJSON( apiBase, {
            action   : 'cargoquery',
            tables   : 'Projects',
            fields   : 'status',
            where    : 'categories HOLDS \'' + hubSlug + '\' AND status IS NOT NULL AND status != \'\'',
            group_by : 'status',
            order_by : 'status',
            limit    : 20,
            format   : 'json'
        } ).done( function ( data ) {
            ( data.cargoquery || [] ).forEach( function ( row ) {
                var v = row.title && row.title.status;
                if ( v ) $status.append( $( '<option>' ).val( v.toLowerCase() ).text( v ) );
            } );
        } );

        $.getJSON( apiBase, {
            action   : 'cargoquery',
            tables   : 'Projects',
            fields   : 'license',
            where    : 'categories HOLDS \'' + hubSlug + '\' AND license IS NOT NULL AND license != \'\'',
            group_by : 'license',
            order_by : 'license',
            limit    : 20,
            format   : 'json'
        } ).done( function ( data ) {
            ( data.cargoquery || [] ).forEach( function ( row ) {
                var v = row.title && row.title.license;
                if ( v ) $license.append( $( '<option>' ).val( v.toLowerCase() ).text( v ) );
            } );
        } );

        // --- Filter + sort ---------------------------------------------------
        function applyFilters() {
            var q = $search.val().toLowerCase().trim();
            var s = $status.val();
            var l = $license.val();
            var visible = 0;

            $cards.each( function () {
                var $c = $( this );
                var hay = [ $c.data('name'), $c.data('tags'), $c.data('creator'), $c.data('extra') ]
                    .join( ' ' ).toLowerCase();
                var show = ( !q || hay.indexOf(q) !== -1 ) &&
                           ( !s || ( $c.data('status')  || '' ).toLowerCase() === s ) &&
                           ( !l || ( $c.data('license') || '' ).toLowerCase() === l );
                $c.toggle( show );
                if ( show ) visible++;
            } );

            $count.text( ( q||s||l ? visible + ' / ' : '' ) + total + ' project' + ( total===1 ? '' : 's' ) );
            $( '.project-hub-empty' ).toggle( visible === 0 );

            var sortVal = $sort.val();
            $cards.filter( ':visible' ).toArray().sort( function ( a, b ) {
                if ( sortVal === 'alpha'        ) return ( $( a ).data('name') || '' ).localeCompare( $( b ).data('name') || '' );
                if ( sortVal === 'edited'       ) return ( $( b ).data('timestamp') || '' ).localeCompare( $( a ).data('timestamp') || '' );
                if ( sortVal === 'contributors' ) return ( +$( b ).data('contributor-count') || 0 ) - ( +$( a ).data('contributor-count') || 0 );
                return 0;
            } ).forEach( function ( el ) { $grid.append( el ); } );
        }

        $search.on( 'input', applyFilters );
        $status.on( 'change', applyFilters );
        $license.on( 'change', applyFilters );
        $sort.on( 'change', applyFilters );

        // --- API calls -------------------------------------------------------
        var pageNames = $cards.map( function () {
            return decodeURIComponent( $( this ).data('page') || '' ).replace( /_/g, ' ' ) || null;
        } ).get().filter( Boolean );

        mw.loader.using( 'mediawiki.api' ).then( function () {
            var api = new mw.Api();

            // Card enrichment: contributor count + last edited
            if ( pageNames.length ) {
                for ( var i = 0; i < pageNames.length; i += 20 ) {
                    ( function ( batch ) {
                        api.get( {
                            action  : 'query',
                            titles  : batch.join( '|' ),
                            prop    : 'revisions|contributors',
                            rvprop  : 'timestamp',
                            rvlimit : 1,
                            pclimit : 20,
                            format  : 'json'
                        } ).done( function ( data ) {
                            if ( !data || !data.query || !data.query.pages ) return;
                            $.each( data.query.pages, function ( _, page ) {
                                if ( page.missing !== undefined ) return;
                                var ts = page.revisions && page.revisions[0] && page.revisions[0].timestamp || '';
                                var n  = ( page.contributors ? page.contributors.length : 0 )
                                       + ( page.anoncontributors || 0 );
                                var $c = $cards.filter( function () {
                                    return decodeURIComponent( $( this ).data('page') || '' ).replace( /_/g, ' ' ) === page.title;
                                } );
                                if ( !$c.length ) return;
                                $c.data( 'timestamp', ts ).data( 'contributor-count', n );
                                var d = ts ? new Date( ts ).toLocaleDateString( 'en-GB', { year: 'numeric', month: 'short' } ) : '—';
                                $c.find( '.project-card-contributors' ).text( n + ( n === 1 ? ' contributor' : ' contributors' ) ).addClass( 'loaded' );
                                $c.find( '.project-card-updated' ).text( 'Edited ' + d ).addClass( 'loaded' );
                            } );
                        } );
                    }( pageNames.slice( i, i + 20 ) ) );
                }
            }

            // Hub header: most recently touched page in this hub's category
            api.get( {
                action  : 'query',
                list    : 'categorymembers',
                cmtitle : 'Category:Projects/' + hubSlug,
                cmsort  : 'timestamp',
                cmdir   : 'desc',
                cmlimit : 1,
                cmtype  : 'page',
                cmprop  : 'ids|title|timestamp',
                format  : 'json'
            } ).done( function ( data ) {
                var members = data.query && data.query.categorymembers;
                if ( !members || !members.length ) return;
                var name = members[0].title.replace( /^.*\//, '' );
                var date = members[0].timestamp
                    ? new Date( members[0].timestamp ).toLocaleDateString( 'en-GB', { day: 'numeric', month: 'short', year: 'numeric' } )
                    : '—';
                var str = name + ' — ' + date;
                $( '#hub-meta-last-submission' ).text( str );
                $( '#hub-meta-last-edit' ).text( str );
            } );
        } );

    } );
}() );