Jump to content

MediaWiki:Gadget-SpecImporter.js

From RetroTechCollection
Revision as of 09:59, 25 March 2026 by Josh (talk | contribs)
(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.
/**
 * RTC Wiki Spec Sheet Importer
 * 
 * Gadget that searches EveryMac.com (via server-side proxy),
 * fetches specs for a given Mac model, and pre-fills the
 * Infobox Generator form or directly generates infobox wikitext.
 * 
 * Requires: SpecImporter.php deployed on the wiki server.
 * Works with: InfoboxGenerator gadget (fills its form fields).
 */
( function ( mw, $ ) {
	'use strict';

	var API_BASE = '/rtc-tools/SpecImporter.php';

	// ============================================================
	// Known EveryMac URL patterns for quick lookup
	// ============================================================
	var FAMILY_PATTERNS = {
		// Compact Macs
		'Macintosh 128K': '/systems/apple/mac_128k/specs/mac_128k.html',
		'Macintosh 512K': '/systems/apple/mac_512k/specs/mac_512k.html',
		'Macintosh 512Ke': '/systems/apple/mac_512ke/specs/mac_512ke.html',
		'Macintosh Plus': '/systems/apple/mac_plus/specs/mac_plus.html',
		'Macintosh SE': '/systems/apple/mac_se/specs/mac_se.html',
		'Macintosh SE/30': '/systems/apple/mac_se/specs/mac_se30.html',
		'Macintosh Classic': '/systems/apple/mac_classic/specs/mac_classic.html',
		'Macintosh Classic II': '/systems/apple/mac_classic/specs/mac_classic_ii.html',
		'Macintosh Color Classic': '/systems/apple/mac_color_classic/specs/mac_color_classic.html',
		// Mac II series
		'Macintosh II': '/systems/apple/mac_ii/specs/mac_ii.html',
		'Macintosh IIx': '/systems/apple/mac_ii/specs/mac_iix.html',
		'Macintosh IIcx': '/systems/apple/mac_ii/specs/mac_iicx.html',
		'Macintosh IIci': '/systems/apple/mac_ii/specs/mac_iici.html',
		'Macintosh IIsi': '/systems/apple/mac_ii/specs/mac_iisi.html',
		'Macintosh IIfx': '/systems/apple/mac_ii/specs/mac_iifx.html',
		'Macintosh IIvi': '/systems/apple/mac_ii/specs/mac_iivi.html',
		'Macintosh IIvx': '/systems/apple/mac_ii/specs/mac_iivx.html'
	};

	// ============================================================
	// Search EveryMac via proxy
	// ============================================================
	function searchModels( query ) {
		return $.ajax( {
			url: API_BASE,
			data: { action: 'search', q: query },
			dataType: 'json',
			timeout: 15000
		} );
	}

	// ============================================================
	// Fetch specs from EveryMac via proxy
	// ============================================================
	function fetchSpecs( urlPath ) {
		return $.ajax( {
			url: API_BASE,
			data: { action: 'fetch', url: urlPath },
			dataType: 'json',
			timeout: 15000
		} );
	}

	// ============================================================
	// Build the Spec Importer UI
	// ============================================================
	function buildUI( $container ) {
		$container.empty();

		var $wrapper = $( '<div>' )
			.attr( 'id', 'rtc-spec-importer' )
			.css( {
				'max-width': '900px',
				'margin': '0 auto',
				'font-family': 'sans-serif'
			} );

		$wrapper.append(
			$( '<h2>' ).text( 'Spec Sheet Importer' ),
			$( '<p>' ).css( 'color', '#555' ).text(
				'Search for a Mac model on EveryMac.com, import its specs, and generate infobox wikitext.'
			)
		);

		// Search bar
		var $searchRow = $( '<div>' ).css( {
			'display': 'flex',
			'gap': '10px',
			'margin-bottom': '20px'
		} );

		var $searchInput = $( '<input>' )
			.attr( {
				type: 'text',
				id: 'rtc-spec-search-input',
				placeholder: 'e.g. Macintosh IIsi, LC 475, Quadra 700...'
			} )
			.css( {
				'flex': '1',
				'padding': '10px 14px',
				'font-size': '15px',
				'border': '1px solid #ccc',
				'border-radius': '4px'
			} );

		var $searchBtn = $( '<button>' )
			.text( 'Search EveryMac' )
			.attr( 'id', 'rtc-spec-search-btn' )
			.css( {
				'padding': '10px 24px',
				'font-size': '14px',
				'font-weight': 'bold',
				'background-color': '#3366cc',
				'color': '#fff',
				'border': 'none',
				'border-radius': '4px',
				'cursor': 'pointer'
			} );

		$searchRow.append( $searchInput, $searchBtn );
		$wrapper.append( $searchRow );

		// Quick-select for known models
		var $quickSelect = $( '<div>' ).css( { 'margin-bottom': '20px' } );
		$quickSelect.append(
			$( '<label>' ).css( 'font-weight', 'bold' ).text( 'Quick select: ' ),
			$( '<select>' )
				.attr( 'id', 'rtc-spec-quick-select' )
				.css( { 'padding': '6px', 'font-size': '14px', 'margin-left': '8px' } )
		);

		var $qSelect = $quickSelect.find( 'select' );
		$qSelect.append( $( '<option>' ).val( '' ).text( 'โ€” Choose a known model โ€”' ) );
		Object.keys( FAMILY_PATTERNS ).sort().forEach( function ( name ) {
			$qSelect.append( $( '<option>' ).val( FAMILY_PATTERNS[ name ] ).text( name ) );
		} );

		$wrapper.append( $quickSelect );

		// Status / loading
		var $status = $( '<div>' )
			.attr( 'id', 'rtc-spec-status' )
			.css( { 'margin-bottom': '15px', 'color': '#666', 'display': 'none' } );
		$wrapper.append( $status );

		// Search results
		var $results = $( '<div>' )
			.attr( 'id', 'rtc-spec-results' )
			.css( { 'display': 'none', 'margin-bottom': '20px' } );
		$wrapper.append( $results );

		// Spec display area
		var $specDisplay = $( '<div>' )
			.attr( 'id', 'rtc-spec-display' )
			.css( { 'display': 'none' } );
		$wrapper.append( $specDisplay );

		$container.append( $wrapper );

		// ========== Events ==========

		// Search
		$searchBtn.on( 'click', function () {
			var query = $searchInput.val().trim();
			if ( !query ) {
				return;
			}

			// Check if it's a known model first
			if ( FAMILY_PATTERNS[ query ] ) {
				doFetch( FAMILY_PATTERNS[ query ] );
				return;
			}

			showStatus( 'Searching EveryMac for "' + query + '"...' );
			$results.hide();
			$specDisplay.hide();

			searchModels( query ).done( function ( data ) {
				if ( data.results && data.results.length > 0 ) {
					showResults( data.results );
				} else {
					showStatus( 'No results found. Try a different search term.' );
				}
			} ).fail( function () {
				showStatus( 'Search failed. You can try entering an EveryMac URL directly.' );
			} );
		} );

		// Enter key to search
		$searchInput.on( 'keypress', function ( e ) {
			if ( e.which === 13 ) {
				$searchBtn.trigger( 'click' );
			}
		} );

		// Quick select
		$qSelect.on( 'change', function () {
			var url = $( this ).val();
			if ( url ) {
				doFetch( url );
			}
		} );
	}

	// ============================================================
	// Show status message
	// ============================================================
	function showStatus( msg ) {
		$( '#rtc-spec-status' ).text( msg ).show();
	}

	// ============================================================
	// Show search results
	// ============================================================
	function showResults( results ) {
		var $results = $( '#rtc-spec-results' ).empty().show();
		$( '#rtc-spec-status' ).hide();

		$results.append( $( '<h3>' ).text( 'Search Results (' + results.length + ')' ) );

		var $list = $( '<ul>' ).css( { 'list-style': 'none', 'padding': '0' } );

		results.forEach( function ( r ) {
			var $li = $( '<li>' ).css( { 'margin-bottom': '8px' } );
			var $link = $( '<a>' )
				.text( r.name )
				.attr( 'href', '#' )
				.css( { 'color': '#3366cc', 'text-decoration': 'none', 'font-size': '14px' } )
				.on( 'click', function ( e ) {
					e.preventDefault();
					doFetch( r.url );
				} );
			$li.append( $link );
			$li.append( $( '<span>' ).css( 'color', '#888' ).text( ' โ€” ' + r.url ) );
			$list.append( $li );
		} );

		$results.append( $list );
	}

	// ============================================================
	// Fetch and display specs
	// ============================================================
	function doFetch( urlPath ) {
		showStatus( 'Fetching specs from EveryMac...' );
		$( '#rtc-spec-results' ).hide();
		$( '#rtc-spec-display' ).hide();

		fetchSpecs( urlPath ).done( function ( data ) {
			if ( data.success ) {
				showSpecs( data );
			} else {
				showStatus( 'Error: ' + ( data.error || 'Unknown error' ) );
			}
		} ).fail( function ( xhr ) {
			var err = 'Fetch failed.';
			try {
				err = JSON.parse( xhr.responseText ).error || err;
			} catch ( e ) {}
			showStatus( err );
		} );
	}

	// ============================================================
	// Display fetched specs with edit capability
	// ============================================================
	function showSpecs( data ) {
		$( '#rtc-spec-status' ).hide();
		var $display = $( '#rtc-spec-display' ).empty().show();

		var specs = data.raw_specs || {};
		var infobox = data.infobox_fields || {};

		// Header
		$display.append(
			$( '<h3>' ).text( 'Imported Specs' ),
			$( '<p>' ).html(
				'<strong>Source:</strong> <a href="' + data.source_url +
				'" target="_blank">' + data.source_url + '</a>'
			)
		);

		// Two columns: raw specs + infobox fields
		var $columns = $( '<div>' ).css( {
			'display': 'flex',
			'gap': '30px',
			'flex-wrap': 'wrap'
		} );

		// Left: Editable infobox fields
		var $left = $( '<div>' ).css( { 'flex': '1', 'min-width': '350px' } );
		$left.append( $( '<h4>' ).text( 'Infobox Fields (editable)' ) );

		var infoboxFields = [
			{ name: 'name', label: 'Model Name' },
			{ name: 'manufacturer', label: 'Manufacturer' },
			{ name: 'type', label: 'Type' },
			{ name: 'release_date', label: 'Release Date' },
			{ name: 'discontinued', label: 'Discontinued' },
			{ name: 'price', label: 'Price' },
			{ name: 'cpu', label: 'CPU' },
			{ name: 'memory', label: 'Memory' },
			{ name: 'storage', label: 'Storage' },
			{ name: 'display', label: 'Display' },
			{ name: 'sound', label: 'Sound' },
			{ name: 'dimensions', label: 'Dimensions' },
			{ name: 'weight', label: 'Weight' },
			{ name: 'os', label: 'OS' },
			{ name: 'model', label: 'Model Number' },
			{ name: 'codename', label: 'Codename' },
			{ name: 'predecessor', label: 'Predecessor' },
			{ name: 'successor', label: 'Successor' },
			{ name: 'image', label: 'Image filename' }
		];

		infoboxFields.forEach( function ( field ) {
			var $row = $( '<div>' ).css( {
				'margin-bottom': '8px',
				'display': 'flex',
				'align-items': 'center'
			} );

			$row.append(
				$( '<label>' ).text( field.label ).css( {
					'width': '130px',
					'flex-shrink': '0',
					'font-size': '13px'
				} ),
				$( '<input>' )
					.attr( { type: 'text', 'data-infobox': field.name } )
					.val( infobox[ field.name ] || '' )
					.css( {
						'flex': '1',
						'padding': '5px 8px',
						'font-size': '13px',
						'border': '1px solid #ccc',
						'border-radius': '3px'
					} )
			);
			$left.append( $row );
		} );

		$columns.append( $left );

		// Right: Raw specs reference
		var $right = $( '<div>' ).css( {
			'flex': '1',
			'min-width': '300px',
			'max-height': '600px',
			'overflow-y': 'auto'
		} );
		$right.append( $( '<h4>' ).text( 'All EveryMac Specs (reference)' ) );

		var $specTable = $( '<table>' ).css( {
			'width': '100%',
			'border-collapse': 'collapse',
			'font-size': '12px'
		} );

		Object.keys( specs ).forEach( function ( key ) {
			if ( key.charAt( 0 ) === '_' ) { return; }
			var $tr = $( '<tr>' );
			$tr.append(
				$( '<td>' ).text( key ).css( {
					'padding': '3px 6px',
					'border-bottom': '1px solid #eee',
					'font-weight': 'bold',
					'white-space': 'nowrap',
					'vertical-align': 'top'
				} ),
				$( '<td>' ).text( specs[ key ] ).css( {
					'padding': '3px 6px',
					'border-bottom': '1px solid #eee'
				} )
			);
			$specTable.append( $tr );
		} );

		$right.append( $specTable );
		$columns.append( $right );
		$display.append( $columns );

		// Buttons
		var $buttons = $( '<div>' ).css( { 'margin-top': '20px' } );

		var $genBtn = $( '<button>' )
			.text( 'Generate Infobox Wikitext' )
			.css( {
				'padding': '10px 24px',
				'font-size': '14px',
				'font-weight': 'bold',
				'background-color': '#3366cc',
				'color': '#fff',
				'border': 'none',
				'border-radius': '4px',
				'cursor': 'pointer',
				'margin-right': '10px'
			} );

		var $fillBtn = $( '<button>' )
			.text( 'Fill Infobox Generator Form' )
			.css( {
				'padding': '10px 24px',
				'font-size': '14px',
				'background-color': '#2d8e36',
				'color': '#fff',
				'border': 'none',
				'border-radius': '4px',
				'cursor': 'pointer',
				'margin-right': '10px'
			} );

		var $insertBtn = $( '<button>' )
			.text( 'Insert into Editor' )
			.css( {
				'padding': '10px 24px',
				'font-size': '14px',
				'background-color': '#6b3fa0',
				'color': '#fff',
				'border': 'none',
				'border-radius': '4px',
				'cursor': 'pointer'
			} );

		$buttons.append( $genBtn, $fillBtn, $insertBtn );
		$display.append( $buttons );

		// Output
		var $output = $( '<div>' ).attr( 'id', 'rtc-spec-output' ).css( {
			'margin-top': '15px',
			'display': 'none'
		} );
		$output.append(
			$( '<h4>' ).text( 'Generated Wikitext' ),
			$( '<textarea>' )
				.attr( { 'id': 'rtc-spec-wikitext', 'rows': 20, 'readonly': true } )
				.css( {
					'width': '100%',
					'font-family': 'monospace',
					'font-size': '13px',
					'padding': '10px',
					'background': '#f5f5f5',
					'border': '1px solid #ccc',
					'border-radius': '4px',
					'box-sizing': 'border-box'
				} )
		);
		$display.append( $output );

		// Events
		$genBtn.on( 'click', function () {
			var values = collectEditedValues();
			var wikitext = buildInfoboxWikitext( values );
			$( '#rtc-spec-wikitext' ).val( wikitext );
			$output.show();
			navigator.clipboard.writeText( wikitext ).then( function () {
				mw.notify( 'Infobox wikitext copied to clipboard!', { type: 'success' } );
			} );
		} );

		$fillBtn.on( 'click', function () {
			// Navigate to the Infobox Generator page with pre-filled data
			var values = collectEditedValues();
			var params = new URLSearchParams();
			params.set( 'prefill', JSON.stringify( values ) );
			window.location.href = mw.util.getUrl( 'Special:BlankPage/InfoboxGenerator' ) + '?' + params.toString();
		} );

		$insertBtn.on( 'click', function () {
			var values = collectEditedValues();
			var wikitext = buildInfoboxWikitext( values );
			var $textarea = $( '#wpTextbox1' );
			if ( $textarea.length ) {
				$textarea.val( wikitext + '\n\n' + $textarea.val() );
				$textarea.trigger( 'change' );
				mw.notify( 'Infobox inserted at the top of the article.', { type: 'success' } );
			} else {
				navigator.clipboard.writeText( wikitext ).then( function () {
					mw.notify( 'Copied to clipboard. Open an article editor and paste.', { type: 'success' } );
				} );
			}
		} );
	}

	// ============================================================
	// Collect edited values from the import form
	// ============================================================
	function collectEditedValues() {
		var values = {};
		$( '#rtc-spec-display input[data-infobox]' ).each( function () {
			var name = $( this ).attr( 'data-infobox' );
			var val = $( this ).val().trim();
			if ( val ) {
				values[ name ] = val;
			}
		} );
		return values;
	}

	// ============================================================
	// Build infobox wikitext from field values
	// ============================================================
	function buildInfoboxWikitext( values ) {
		var lines = [ '{{Infobox computer' ];
		var fieldOrder = [
			'name', 'image', 'caption', 'logo', 'developer', 'manufacturer', 'type',
			'release_date', 'discontinued', 'price', 'units_sold',
			'cpu', 'memory', 'storage', 'display', 'sound',
			'dimensions', 'weight', 'os',
			'predecessor', 'successor', 'codename', 'model'
		];

		fieldOrder.forEach( function ( key ) {
			var val = values[ key ];
			if ( !val ) { return; }

			// Format image
			if ( key === 'image' ) {
				if ( val.indexOf( '[[' ) === -1 ) {
					val = '[[File:' + val + '|250px]]';
				}
			}
			// Format predecessor/successor as links
			if ( ( key === 'predecessor' || key === 'successor' ) && val.indexOf( '[[' ) === -1 ) {
				val = '[[' + val + ']]';
			}

			var paramName = ( key === 'release_date' ) ? 'release date' : key;
			var pad = '             '.substring( 0, Math.max( 1, 14 - paramName.length ) );
			lines.push( '| ' + paramName + pad + '= ' + val );
		} );

		lines.push( '}}' );
		return lines.join( '\n' );
	}

	// ============================================================
	// Special page init
	// ============================================================
	function initSpecialPage() {
		if (
			mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Blankpage' &&
			window.location.href.indexOf( 'SpecImporter' ) !== -1
		) {
			var $content = $( '#mw-content-text' );
			$content.empty();
			document.title = 'Spec Sheet Importer โ€“ ' + mw.config.get( 'wgSiteName' );
			$( '#firstHeading' ).text( 'Spec Sheet Importer' );
			buildUI( $content );
		}
	}

	// ============================================================
	// Sidebar link
	// ============================================================
	function addSidebarLink() {
		mw.util.addPortletLink(
			'p-tb',
			mw.util.getUrl( 'Special:BlankPage/SpecImporter' ),
			'Spec Importer',
			't-spec-importer',
			'Import specs from EveryMac.com'
		);
	}

	// ============================================================
	// Init
	// ============================================================
	$( function () {
		addSidebarLink();
		initSpecialPage();
	} );

}( mediaWiki, jQuery ) );