Actions

MediaWiki

MediaWiki:Gadget-JourneySystem.js

From Dune Awakening DB

Revision as of 10:36, 2 June 2025 by Operator (talk | contribs) (Created page with "Journey System – ResourceLoader gadget * Dependencies: mediawiki.util, jquery * Last copied from MediaWiki:Common.js on 2025-06-02: ( function ( mw, $ ) { 'use strict'; // ── Abort if the page has no journey markup ──────────────────────────────── if ( !$( '.journey-main-container' ).length ) { return; } // ── BEGIN: original common.js content (unchanged) ────────...")
(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.
/* Journey System – ResourceLoader gadget
 * Dependencies: mediawiki.util, jquery
 * Last copied from MediaWiki:Common.js on 2025-06-02
 */
( function ( mw, $ ) {
	'use strict';

	// ── Abort if the page has no journey markup ────────────────────────────────
	if ( !$( '.journey-main-container' ).length ) {
		return;
	}

	// ── BEGIN: original common.js content (unchanged) ──────────────────────────
	// ▼▼▼  Delete only the first line “(function($, mw){ …” and the very last
	//      line “})(jQuery, mediaWiki);” from your old file, then paste the rest
	//      right here.  Everything else stays exactly the same.
	// ……………………………………………………………………………………………………………………………

	
/**
 * Journey System JavaScript for MediaWiki
 * Enhanced with proper task counting and initialization
 */

    'use strict';
    console.log('%c[JourneySystem] common.js loaded','color:teal;font-weight:bold');
    // Only run on pages with journey system
    if (!$('.journey-main-container').length) return;
    
    window.DuneJourneySystem = {
        // Store current active journey
        currentActiveJourney: null,
        journeyData: {},
        
        // Initialize the system
        init: function() {
            var self = this;
            console.log('[JourneySystem] init() called');

            // Parse journey data from the page
            self.parseJourneyData();
            
            // Set up event listeners
            self.setupEventListeners();
            
            // Check for URL hash
            self.checkUrlHash();
        },
        
        setInitialAccordionState: function() {
            console.log('[JourneySystem] Setting initial accordion state');
            
            // Add collapsed class to all objectives first
            $('.objective-item').addClass('collapsed');
            
            // Remove collapsed class from first objective only
            $('.objective-item:first').removeClass('collapsed');
        },
        
        // Parse journey data embedded in the page
        parseJourneyData: function() {
            var self = this;
            
            // Journey data is embedded as JSON in a script tag
            var dataElement = $('#journey-data-json');
            if (dataElement.length) {
                try {
                    self.journeyData = JSON.parse(dataElement.text());
                } catch (e) {
                    console.error('Failed to parse journey data:', e);
                }
            }
            
            // Alternative: Use data from HTML attributes if JSON parsing fails
            if (!self.journeyData || Object.keys(self.journeyData).length === 0) {
                if (window.journeyDataFromHTML) {
                    self.journeyData = window.journeyDataFromHTML;
                }
            }
        },
        
        // Set up all event listeners
        setupEventListeners: function() {
            var self = this;
            
            // Journey card clicks
            $(document).on('click', '.journey-card', function(e) {
                e.preventDefault();
                var journeyId = $(this).data('journey-id');
                self.loadJourneyDetails(journeyId);
            });
            
            // Objective accordion
            $(document).on('click', '.objective-header', function(e) {
                e.preventDefault();
                e.stopPropagation();
                
                console.log('[Toggle] Click detected');
                
                var $objectiveItem = $(this).closest('.objective-item');
                
                console.log('[Toggle] Current state:', $objectiveItem.hasClass('collapsed'));
                
                // Just toggle this item - don't touch others!
                $objectiveItem.toggleClass('collapsed');
                
                console.log('[Toggle] New state:', $objectiveItem.hasClass('collapsed') ? 'closed' : 'open');
            });
                            
            // Prevent clicks on progress from toggling accordion
            $(document).on('click', '.objective-progress', function(e) {
                e.stopPropagation();
            });
            
            // Task checkboxes
            $(document).on('click', '.task-checkbox', function(e) {
                e.stopPropagation();
                $(this).toggleClass('completed');
                self.updateProgress();
            });
            
            // Material checkboxes
            $(document).on('click', '.material-item', function(e) {
                var checkbox = $(this).find('.material-checkbox');
                checkbox.toggleClass('completed');
                $(this).toggleClass('checked');
                self.updateMaterialProgress();
            });
            
            // Description toggle
            $(document).on('click', '.description-toggle', function(e) {
                e.preventDefault();
                var desc = $(this).closest('.journey-description');
                if (desc.hasClass('collapsed')) {
                    desc.removeClass('collapsed').addClass('expanded');
                    $(this).text('Less');
                } else {
                    desc.addClass('collapsed').removeClass('expanded');
                    $(this).text('More');
                }
            });
            
            // Action buttons  –– use delegation so it still works after re-render
            $(document).on('click', '.journey-actions .view-items', function (e) {
                e.preventDefault();
                e.stopPropagation();
                console.log('[JourneySystem] View Items button clicked');
                self.showItemPrepList(e);
                return false;
            });
            
            $(document).on('click', '.journey-actions .view-guide', function (e) {
                e.preventDefault();
                e.stopPropagation();
                console.log('[JourneySystem] View Guide button clicked');
                self.showJourneyGuide(e);
                return false;
            });

            // Popup close
            $(document).on('click', '.popup-close, .journey-popup-overlay', function(e) {
                if (e.target === this || $(e.target).hasClass('popup-close')) {
                    $('.journey-popup-overlay').removeClass('active');
                    setTimeout(function() {
                        $('.journey-popup-overlay').remove();
                    }, 300);
                }
            });
        },
        
        // Journey objective subtotals
        fixTaskObjectiveIds: function() {
            console.log('[JourneySystem] Fixing task objective IDs...');
            
            $('.objective-item').each(function() {
                var $objective = $(this);
                var objectiveId = $objective.find('.objective-header').attr('data-objective-id');
                
                // Set the objective ID on all tasks within this objective
                $objective.find('.task-item').attr('data-objective-id', objectiveId);
                
                console.log('Fixed objective', objectiveId, 'with', $objective.find('.task-item').length, 'tasks');
            });
        },
        
        // Initialize task counts when journey loads
        initializeTaskCounts: function() {
            var self = this;
            
            // Count tasks for each objective on initial load
            $('.objective-item').each(function() {
                var objective = $(this);
                var totalTasks = objective.find('.task-item').length;
                var progressEl = objective.find('.objective-progress');
                
                // Set initial count (0 completed out of total)
                progressEl.text('0/' + totalTasks);
            });
        },
        
        // Load journey details via AJAX
        loadJourneyDetails: function(journeyId) {
            var self = this;
            
            // Update active card styling
            $('.journey-card').removeClass('active');
            $('.journey-card[data-journey-id="' + journeyId + '"]').addClass('active');
            
            // Show loading state
            var panel = $('#journeyDetailsPanel');
            panel.addClass('active loading');
            
            // Make AJAX request to get journey details
            $.ajax({
                url: mw.util.wikiScript('api'),
                data: {
                    action: 'parse',
                    format: 'json',
                    text: '{{JourneyDetails|id=' + journeyId + '}}',
                    contentmodel: 'wikitext'
                },
                success: function(response) {
                    if (response.parse && response.parse.text) {
                        var content = response.parse.text['*'];
                        panel.html(content);
                        panel.removeClass('loading');
                        console.log('[JourneySystem] AJAX success – journey', journeyId, 'loaded');
                        
                        // Store current journey data
                        self.currentActiveJourney = self.findJourneyById(journeyId);
                        console.log('[JourneySystem] Current active journey set to:', self.currentActiveJourney);
                        
                        // Give DOM time to settle before counting
                        setTimeout(function() {
                            $('[class*="objective-item"]').each(function() {
                                var currentClass = $(this).attr('class');
                                if (currentClass.includes('objective-itemcollapsed')) {
                                    $(this).attr('class', 'objective-item collapsed');
                                }
                            });
                            
                            // Initialize task counts
                            self.initializeTaskCounts();
                            self.updateProgress();
                            
                            // Check for long descriptions
                            self.checkDescriptionLength();
                            
                            // Set initial accordion state (first open, others closed)
                            self.setInitialAccordionState();
                        }, 150);
                        
                        // Update URL hash using slug
                        // Use clean slug from data attribute
						var card = $('.journey-card[data-journey-id="' + journeyId + '"]');
						var slug = card.attr('data-slug');
						if (slug) {
						    window.location.hash = slug;
						    console.log('[JourneySystem] Updated URL to slug:', slug);
						} else if (journeyId) {
						    window.location.hash = 'journey-' + journeyId;
						    console.log('[JourneySystem] No slug found, using ID:', journeyId);
						}
                    }
                },
                error: function() {
                    panel.removeClass('loading');
                    panel.html('<div class="error">Failed to load journey details</div>');
                }
            });
        },
        
        // Find journey by ID in data
        findJourneyById: function(journeyId) {
            for (var group in this.journeyData) {
                var journeys = this.journeyData[group];
                for (var i = 0; i < journeys.length; i++) {
                    if (journeys[i].id == journeyId) {
                        return journeys[i];
                    }
                }
            }
            return null;
        },
        
        // Check and add toggle to long descriptions
        checkDescriptionLength: function() {
            $('.journey-description').each(function() {
                var desc = $(this);
                var text = desc.text();
                if (text.length > 100 && !desc.find('.description-toggle').length) {
                    desc.addClass('collapsed');
                    desc.append('<button class="description-toggle">More</button>');
                }
            });
        },
        
        // Update progress counters with proper task counting
        updateProgress: function() {
            console.log('[JourneySystem] updateProgress() called');
            
            // Small delay to ensure DOM is ready
            setTimeout(function() {
                // Per-objective counts (using attr instead of data for fresh elements)
                $('.objective-progress').each(function() {
                    var $prog = $(this);
                    var objId = $prog.attr('data-objective-id');
                    
                    // Find all tasks with this objective ID
                    var $tasks = $('.task-item[data-objective-id="' + objId + '"]');
                    var totalTasks = $tasks.length;
                    var completedTasks = $tasks.find('.task-checkbox.completed').length;
                    
                    console.log(
                        '[JourneySystem] Objective', objId,
                        '→', completedTasks + '/' + totalTasks,
                        'Found tasks:', $tasks.length
                    );
                    
                    // Update the UI
                    $prog.text(completedTasks + '/' + totalTasks);
                    $prog.toggleClass('completed', completedTasks === totalTasks && totalTasks > 0);
                });
                
                // Overall completion
                var total = $('.task-item').length;
                var done = $('.task-checkbox.completed').length;
                var percent = total ? Math.round((done / total) * 100) : 0;
                console.log('[JourneySystem] Overall →', done + '/' + total, percent + '%');
                $('.completion-status').text(percent + '% COMPLETE');
                
                // Save progress if user is logged in
                if (mw.config.get('wgUserName')) {
                    this.saveProgress();
                }
            }.bind(this), 100); // 100ms delay to ensure DOM is ready
        },
        
        // UPDATED showItemPrepList with kebab-case fix
        showItemPrepList: function(e) {
            console.log('[JourneySystem] showItemPrepList called');
            
            if (e) {
                e.preventDefault();
                e.stopPropagation();
            }
            
            var self = this;
            var journeyId = null;
            var journeyTitle = '';
            
            // IMPORTANT: Remove ALL existing popups first
            $('.journey-popup, .journey-popup-overlay').remove();
            console.log('Removed existing popups');
            
            // Get journey info
            var $button = $(e ? e.currentTarget : '.journey-actions .view-items').first();
            var $card = $button.closest('.journey-card');
            
            // Try to find from active card (with kebab-case - this is the fix!)
            var $activeCard = $('.journey-card.active');
            if ($activeCard.length > 0) {
                journeyId = $activeCard.data('journey-id') || $activeCard.attr('data-journey-id');
                journeyTitle = $activeCard.find('.journey-title').text();
                console.log('Found from active card:', journeyId, journeyTitle);
            }
            
            // From URL hash if not found
            if (!journeyId && window.location.hash) {
			    var hash = window.location.hash.replace('#', '');
			    
			    // Check if it's numeric
			    if (/^\d+$/.test(hash)) {
			        journeyId = hash;
			    } else {
			        // Find by slug
			        var card = $('.journey-card[data-slug="' + hash + '"]');
			        if (card.length > 0) {
			            journeyId = card.data('journey-id');
			            journeyTitle = card.find('.journey-name').text();
			        }
			    }
			}
            
            if (!journeyId) {
                console.error('No journey ID found!');
                alert('Please click on a journey card first to select it.');
                return false;
            }
            
            journeyTitle = journeyTitle || 'Journey ' + journeyId;
            console.log('Creating popup for journey:', journeyId);
            
            // Create new popup with unique IDs to avoid conflicts
            var timestamp = Date.now();
            var overlayId = 'popup-overlay-' + timestamp;
            var popupId = 'popup-' + timestamp;
            
            var $overlay = $('<div>')
                .attr('id', overlayId)
                .addClass('journey-popup-overlay')
                .css({
                    position: 'fixed',
                    top: 0,
                    left: 0,
                    width: '100%',
                    height: '100%',
                    background: 'rgba(0, 0, 0, 0)',
                    zIndex: 9999,
                    display: 'block'
                });
            
            var $popup = $('<div>')
                .attr('id', popupId)
                .addClass('journey-popup items-popup')
                .css({
                    position: 'fixed',
                    top: '50%',
                    left: '50%',
                    transform: 'translate(-50%, -50%) scale(0.9)',
                    background: '#2a2a2a',
                    border: '2px solid #fce7c8',
                    padding: '20px',
                    zIndex: 10000,
                    minWidth: '500px',
                    maxWidth: '80%',
                    maxHeight: '80%',
                    overflowY: 'auto',
                    opacity: 0,
                    borderRadius: '5px'
                })
                .html(
                    '<div class="popup-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">' +
                        '<h3 class="popup-title" style="color: #fce7c8; margin: 0;">Item Prep List - ' + journeyTitle + '</h3>' +
                        '<button class="popup-close" style="background: none; border: none; color: #fce7c8; font-size: 28px; cursor: pointer; padding: 0;">×</button>' +
                    '</div>' +
                    '<div class="popup-content"><div class="loading" style="text-align: center; padding: 40px; color: #999;">Loading materials...</div></div>'
                );
            
            // Add to body
            $('body').append($overlay);
            $overlay.append($popup);
            
            // Animate in
            setTimeout(function() {
                $overlay.css('background', 'rgba(0, 0, 0, 0.8)');
                $popup.css({
                    transform: 'translate(-50%, -50%) scale(1)',
                    opacity: 1
                });
            }, 10);
            
            // Close handlers
            var closePopup = function() {
                $overlay.css('background', 'rgba(0, 0, 0, 0)');
                $popup.css({
                    transform: 'translate(-50%, -50%) scale(0.9)',
                    opacity: 0
                });
                setTimeout(function() {
                    $('#' + overlayId).remove();
                }, 300);
            };
            
            $overlay.on('click', function(e) {
                if (e.target === this) {
                    closePopup();
                }
            });
            
            $popup.find('.popup-close').on('click', closePopup);
            
            // Load materials
            $.ajax({
                url: mw.util.wikiScript('api'),
                data: {
                    action: 'parse',
                    format: 'json',
                    text: '{{JourneyMaterials|id=' + journeyId + '}}',
                    contentmodel: 'wikitext'
                },
                success: function(response) {
                    if (response.parse && response.parse.text) {
                        var content = response.parse.text['*'];
                        $popup.find('.popup-content').html(content);
                    } else {
                        $popup.find('.popup-content').html(
                            '<div style="color: #ff6b6b; padding: 20px;">Unable to load materials.</div>'
                        );
                    }
                },
                error: function(xhr, status, error) {
                    $popup.find('.popup-content').html(
                        '<div style="color: #ff6b6b; padding: 20px;">' +
                            '<p><strong>Error loading materials</strong></p>' +
                            '<p>' + error + '</p>' +
                        '</div>'
                    );
                }
            });
            
            return false;
        },
        
        // Add this helper function after showItemPrepList
        getExampleMaterialsDisplay: function() {
            return `
                <div class="materials-container">
                    <div class="materials-tier level-1">
                        <div class="tier-header">Direct Materials Required</div>
                        <div class="material-item">
                            <span class="material-name">Duraluminum Ingot</span>
                            <span class="material-qty">1</span>
                        </div>
                        <div class="material-item">
                            <span class="material-name">Agave Seeds</span>
                            <span class="material-qty">10</span>
                        </div>
                        <div class="material-item">
                            <span class="material-name">Water</span>
                            <span class="material-qty">170</span>
                        </div>
                    </div>
                    
                    <div class="materials-tier level-2">
                        <div class="tier-header">Components for Level 1 Materials</div>
                        <div class="material-item">
                            <span class="material-name">Aluminum Ingot</span>
                            <span class="material-qty">2</span>
                        </div>
                        <div class="material-item">
                            <span class="material-name">Jasmium Crystal</span>
                            <span class="material-qty">7</span>
                        </div>
                        <div class="material-item">
                            <span class="material-name">Water</span>
                            <span class="material-qty">1300</span>
                        </div>
                    </div>
                    
                    <div class="materials-tier level-3">
                        <div class="tier-header">Base Resources Needed</div>
                        <div class="material-item">
                            <span class="material-name">Aluminum Ore</span>
                            <span class="material-qty">22</span>
                        </div>
                        <div class="material-item">
                            <span class="material-name">Water</span>
                            <span class="material-qty">800</span>
                        </div>
                    </div>
                    
                    <div class="materials-summary">
                        <p>Note: This is example data. The database view 'vw_recipe_inputs_by_level' needs to be configured.</p>
                    </div>
                </div>
            `;
        },
        
        // NEW showJourneyGuide function (replaces showVideoGuide)
        showJourneyGuide: function(e) {
            console.log('[JourneySystem] showJourneyGuide called');
            
            if (e) {
                e.preventDefault();
                e.stopPropagation();
            }
            
            var self = this;
            var journeyId = null;
            var journeyTitle = '';
            
            // Remove existing popups
            $('.journey-popup, .journey-popup-overlay').remove();
            
            // Use the same logic to find journey ID
            var $button = $(e ? e.currentTarget : '.journey-actions .view-guide').first();
            
            // Try to find from active card (with kebab-case)
            var $activeCard = $('.journey-card.active');
            if ($activeCard.length > 0) {
                journeyId = $activeCard.data('journey-id') || $activeCard.attr('data-journey-id');
                journeyTitle = $activeCard.find('.journey-title').text();
                console.log('Found from active card:', journeyId, journeyTitle);
            }
            
            // From URL hash
            if (!journeyId && window.location.hash) {
			    var hash = window.location.hash.replace('#', '');
			    
			    // Check if it's numeric
			    if (/^\d+$/.test(hash)) {
			        journeyId = hash;
			    } else {
			        // Find by slug
			        var card = $('.journey-card[data-slug="' + hash + '"]');
			        if (card.length > 0) {
			            journeyId = card.data('journey-id');
			            journeyTitle = card.find('.journey-name').text();
			        }
			    }
			}
            
            if (!journeyId) {
                console.error('Could not find journey ID!');
                alert('Please click on a journey card first to select it.');
                return false;
            }
            
            journeyTitle = journeyTitle || 'Journey ' + journeyId;
            console.log('Creating guide popup for journey:', journeyId, journeyTitle);
            
            // Create popup (same structure as items popup)
            var timestamp = Date.now();
            var overlayId = 'popup-overlay-' + timestamp;
            var popupId = 'popup-' + timestamp;
            
            var $overlay = $('<div>')
                .attr('id', overlayId)
                .addClass('journey-popup-overlay')
                .css({
                    position: 'fixed',
                    top: 0,
                    left: 0,
                    width: '100%',
                    height: '100%',
                    background: 'rgba(0, 0, 0, 0)',
                    zIndex: 9999,
                    display: 'block'
                });
            
            var $popup = $('<div>')
                .attr('id', popupId)
                .addClass('journey-popup guide-popup')
                .css({
                    position: 'fixed',
                    top: '50%',
                    left: '50%',
                    transform: 'translate(-50%, -50%) scale(0.9)',
                    background: '#2a2a2a',
                    border: '2px solid #fce7c8',
                    padding: '20px',
                    zIndex: 10000,
                    minWidth: '600px',
                    maxWidth: '90%',
                    maxHeight: '90%',
                    overflowY: 'auto',
                    opacity: 0,
                    borderRadius: '5px',
                    boxShadow: '0 4px 20px rgba(0,0,0,0.5)'
                })
                .html(
                    '<div class="popup-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid rgba(252,231,200,0.2);">' +
                        '<h3 class="popup-title" style="color: #fce7c8; margin: 0; font-size: 1.4em;">Journey Guide - ' + journeyTitle + '</h3>' +
                        '<button class="popup-close" style="background: none; border: none; color: #fce7c8; font-size: 28px; cursor: pointer; padding: 0; line-height: 1;">×</button>' +
                    '</div>' +
                    '<div class="popup-content"><div class="loading" style="text-align: center; padding: 40px; color: #999;">Loading guide...</div></div>'
                );
            
            $('body').append($overlay);
            $overlay.append($popup);
            
            setTimeout(function() {
                $overlay.css('background', 'rgba(0, 0, 0, 0.8)');
                $popup.css({
                    transform: 'translate(-50%, -50%) scale(1)',
                    opacity: 1
                });
            }, 10);
            
            var closePopup = function() {
                $overlay.css('background', 'rgba(0, 0, 0, 0)');
                $popup.css({
                    transform: 'translate(-50%, -50%) scale(0.9)',
                    opacity: 0
                });
                setTimeout(function() {
                    $('#' + overlayId).remove();
                }, 300);
            };
            
            $overlay.on('click', function(e) {
                if (e.target === this) {
                    closePopup();
                }
            });
            
            $popup.find('.popup-close').on('click', closePopup);
            
            // Load guide
            $.ajax({
                url: mw.util.wikiScript('api'),
                data: {
                    action: 'parse',
                    format: 'json',
                    text: '{{JourneyGuide|id=' + journeyId + '}}',
                    contentmodel: 'wikitext'
                },
                success: function(response) {
                    if (response.parse && response.parse.text) {
                        var content = response.parse.text['*'];
                        $popup.find('.popup-content').html(content);
                    } else {
                        $popup.find('.popup-content').html(
                            '<div style="color: #ff6b6b; padding: 20px;">Guide not available for this journey.</div>'
                        );
                    }
                },
                error: function(xhr, status, error) {
                    $popup.find('.popup-content').html(
                        '<div style="color: #ff6b6b; padding: 20px;">' +
                            '<p><strong>Error loading guide</strong></p>' +
                            '<p>' + error + '</p>' +
                        '</div>'
                    );
                }
            });
            
            return false;
        },
        
        // Create popup structure
        createPopup: function(className, title) {
            var popup = $('<div class="journey-popup-overlay">' +
                         '<div class="journey-popup ' + className + '">' +
                         '<div class="popup-header">' +
                         '<h3 class="popup-title">' + title + '</h3>' +
                         '<button class="popup-close">×</button>' +
                         '</div>' +
                         '<div class="popup-content"></div>' +
                         '</div></div>');
            return popup;
        },
        
        // Show popup with animation
        showPopup: function(popup) {
            $('body').append(popup);
            setTimeout(function() {
                popup.addClass('active');
            }, 10);
        },
        
        // Update material progress in popup
        updateMaterialProgress: function () {
            const total = $('.material-item').length;          // every row
            const done  = $('.material-item.checked').length;  // ticked rows

            // numbers
            $('.materials-progress-count').text(`${done}/${total}`);

            // bar width
            const pct = total ? (done / total) * 100 : 0;
            $('.progress-bar').css('width', pct + '%');

            // glow on 100 %
            $('.materials-progress-header')
                .toggleClass('completed', done === total && total > 0);
        },
        
        // Check URL hash on load
        checkUrlHash: function() {
		    const hash = window.location.hash.replace('#', '');
		    if (!hash) return;
		    
		    console.log('[JourneySystem] Checking URL hash:', hash);
		    
		    // First check if it's a numeric ID (with or without prefix)
		    var journeyMatch = hash.match(/^journey-(\d+)$/);
		    if (journeyMatch) {
		        this.loadJourneyDetails(journeyMatch[1]);
		        return;
		    }
		    
		    // Check if it's just a number
		    if (/^\d+$/.test(hash)) {
		        this.loadJourneyDetails(hash);
		        return;
		    }
		    
		    // Otherwise, treat as slug - find journey card with matching data-slug
		    var card = $('.journey-card[data-slug="' + hash + '"]');
		    if (card.length > 0) {
		        var journeyId = card.data('journey-id');
		        console.log('[JourneySystem] Found journey by slug:', hash, 'ID:', journeyId);
		        this.loadJourneyDetails(journeyId);
		    } else {
		        console.log('[JourneySystem] No journey found for slug:', hash);
		    }
		},

        // Placeholder for save progress functionality
        saveProgress: function() {
            // This would save to user preferences or a custom API
            console.log('[JourneySystem] Progress save called (not implemented)');
        }
    };
    
    // Initialize when DOM is ready
    $(function() {
        console.log('[JourneySystem] DOM ready – calling init()');
        DuneJourneySystem.init();
    });
    

/**
 * Enhanced Journey Materials System
 * Provides hierarchical material tracking with drill-up functionality
 */
window.EnhancedMaterialsSystem = {
    // Material relationships mapping (would be populated from database)
    materialRelationships: {},
    
    // Initialize the enhanced materials system
    init: function() {
        console.log('[EnhancedMaterials] Initializing...');
        
        // Set up event listeners
        this.setupEventListeners();
        
        // Initialize counts
        this.updateProgressBar();
        
        // Map material relationships (this would come from your database)
        this.mapMaterialRelationships();
    },
    
    // Set up all event listeners
    setupEventListeners: function() {
        var self = this;
        
        // Material checkbox changes
        $(document).on('change', '.material-checkbox', function() {
            var $checkbox = $(this);
            var $card = $checkbox.closest('.material-card');
            var itemId = $card.data('item');
            var level = parseInt($card.data('level'));
            
            // Toggle checked state
            $card.toggleClass('checked', $checkbox.is(':checked'));
            
            // Handle drill-up functionality
            if ($checkbox.is(':checked') && level > 1) {
                self.checkParentMaterials(itemId, level);
            }
            
            // Update progress
            self.updateProgressBar();
        });
        
        // Card click to toggle checkbox
        $(document).on('click', '.material-card', function () {
            const $card = $(this);
            const $box  = $card.find('.material-checkbox');
        
            $box.toggleClass('completed');      // visual tick
            $card.toggleClass('checked');       // dim / strike-through
            EnhancedMaterialsSystem.updateProgressBar();
        });
        
        // Check/Uncheck all buttons
        $(document).on('click', '.check-all-btn', function() {
            $('.material-checkbox').prop('checked', true).trigger('change');
        });
        
        $(document).on('click', '.uncheck-all-btn', function() {
            $('.material-checkbox').prop('checked', false).trigger('change');
        });
        
        // Toggle view button
        $(document).on('click', '.toggle-view-btn', function() {
            var $btn = $(this);
            var $container = $('.materials-enhanced-container');
            var currentView = $btn.data('view');
            
            if (currentView === 'grid') {
                $container.addClass('list-view');
                $btn.data('view', 'list');
                $btn.html('<span class="icon">⊞</span> Grid View');
            } else {
                $container.removeClass('list-view');
                $btn.data('view', 'grid');
                $btn.html('<span class="icon">📋</span> List View');
            }
        });
    },
    
    // Map material relationships from the data
    mapMaterialRelationships: function() {
        var self = this;
        
        // Use the actual data loaded from the database
        if (window.journeyMaterialRelationships) {
            // Convert the flat structure to parent->children mapping
            self.materialRelationships = {};
            
            for (var child in window.journeyMaterialRelationships) {
                var parent = window.journeyMaterialRelationships[child].parent;
                
                if (!self.materialRelationships[child]) {
                    self.materialRelationships[child] = [];
                }
                
                if (parent && !self.materialRelationships[child].includes(parent)) {
                    self.materialRelationships[child].push(parent);
                }
            }
        }
        
        // Mark cards that have children
        this.markCardsWithChildren();
    },
    
    // Mark cards that have child materials
    markCardsWithChildren: function() {
        var self = this;
        
        $('.material-card').each(function() {
            var $card = $(this);
            var itemId = $card.data('item');
            
            // Check if this material is used in other recipes
            var hasChildren = false;
            for (var child in self.materialRelationships) {
                if (self.materialRelationships[child].includes(itemId)) {
                    hasChildren = true;
                    break;
                }
            }
            
            if (hasChildren) {
                $card.addClass('has-children');
            }
        });
    },
    
    // Check parent materials when a child is checked
    checkParentMaterials: function(childItemId, childLevel) {
        var self = this;
        
        // Find all materials that use this child material
        var parents = this.materialRelationships[childItemId] || [];
        
        parents.forEach(function(parentId) {
            var $parentCard = $('.material-card[data-item="' + parentId + '"]');
            if ($parentCard.length) {
                // Check if all components for this parent are checked
                if (self.areAllComponentsChecked(parentId)) {
                    var $parentCheckbox = $parentCard.find('.material-checkbox');
                    if (!$parentCheckbox.is(':checked')) {
                        $parentCheckbox.prop('checked', true);
                        $parentCard.addClass('checked has-parent-checked');
                        
                        // Recursively check parents of this parent
                        var parentLevel = parseInt($parentCard.data('level'));
                        if (parentLevel > 1) {
                            self.checkParentMaterials(parentId, parentLevel);
                        }
                    }
                }
            }
        });
    },
    
    // Check if all components for a material are checked
    areAllComponentsChecked: function(materialId) {
        var requiredComponents = this.getRequiredComponents(materialId);
        
        for (var i = 0; i < requiredComponents.length; i++) {
            var component = requiredComponents[i];
            var $componentCard = $('.material-card[data-item="' + component.child + '"]');
            if ($componentCard.length && !$componentCard.find('.material-checkbox').is(':checked')) {
                return false;
            }
        }
        
        return true;
    },
    
    // Get required components for a material (from actual database data)
    getRequiredComponents: function(materialId) {
        // Use the data loaded from the database
        if (window.materialRequirements && window.materialRequirements[materialId]) {
            return window.materialRequirements[materialId];
        }
        return [];
    },
    
    // Update the progress bar
    updateProgressBar: function () {
        const total    = $('.material-card').length;
        const checked  = $('.material-card.checked').length;
        const pct      = total ? (checked / total) * 100 : 0;
      
        $('.progress-bar').css('width', pct + '%');
        $('.progress-count').text(checked);
        $('.total-count').text(total);
        $('.materials-progress-header').toggleClass('completed', pct === 100);
    },
    
    // Visual connection lines between related materials (optional)
    drawConnections: function() {
        // This could draw SVG lines between connected materials
        // for a more visual representation of the hierarchy
    }
};

// Patch: make span-based checkboxes work
window.EnhancedMaterialsSystem.setupEventListeners = function () {
  const self = this;

  // 1 ▸ Toggle by clicking the card OR the faux checkbox
  $(document).on('click', '.material-card, .material-checkbox', function (e) {
    // get the card and its checkbox span
    const $card = $(e.target).closest('.material-card');
    const $box  = $card.find('.material-checkbox');

    // toggle visual state
    const newState = !$box.hasClass('completed');
    $box.toggleClass('completed', newState);
    $card.toggleClass('checked', newState);

    // drill-up logic (only when turning ON)
    if (newState && $card.data('level') > 1) {
      self.checkParentMaterials($card.data('item'), $card.data('level'));
    }

    self.updateProgressBar();
  });

  // 2 ▸ Bulk buttons  (optional – remove if you don't need them)
  $(document).on('click', '.check-all-btn',  () => $('.material-checkbox').addClass('completed').trigger('click'));
  $(document).on('click', '.uncheck-all-btn',() => $('.material-checkbox').removeClass('completed').trigger('click'));
};

// 3 ▸ Progress bar now counts .completed spans, not :checked inputs
window.EnhancedMaterialsSystem.updateProgressBar = function () {
  const total    = $('.material-item').length;
  const ready    = $('.material-item.checked').length;
  const pct      = total ? (ready / total) * 100 : 0;

  $('.progress-bar').css('width', pct + '%');
  $('.progress-count').text(ready);
  $('.total-count').text(total);
  $('.materials-progress-header').toggleClass('completed', pct === 100);
};

// Hook into the existing journey system
$(document).ready(function() {
    // Initialize when materials popup is opened
    $(document).on('DOMNodeInserted', '.materials-enhanced-container', function() {
        setTimeout(function() {
            EnhancedMaterialsSystem.init();
        }, 100);
    });
});

/**
 * Journey Card Background Image Handler
 * Extracts MediaWiki file URLs and applies them as background images
 */
(function() {
    'use strict';
    
    /**
     * Apply background images to journey cards
     */
    function applyJourneyBackgrounds() {
        // Find all journey cards with data-icon attribute
        const journeyCards = document.querySelectorAll('.journey-card[data-icon]');
        
        journeyCards.forEach(function(card) {
            const iconFile = card.getAttribute('data-icon');
            
            // Skip if no icon file specified
            if (!iconFile || iconFile === '' || iconFile === 'NULL' || iconFile === 'null') {
                return;
            }
            
            // Check if it ends with .png or .jpg
            if (!iconFile.match(/\.(png|jpg|jpeg)$/i)) {
                return;
            }
            
            // Look for the hidden MediaWiki file element
            const bgSource = card.querySelector('.journey-bg-source img');
            
            if (bgSource) {
                // Get the src URL from the img element
                const imageUrl = bgSource.getAttribute('src');
                
                if (imageUrl) {
                    // Apply as background image
                    card.style.backgroundImage = 'url(' + imageUrl + ')';
                    card.classList.add('has-background');
                    
                    // Remove the has-icon class if it exists to avoid conflicts
                    card.classList.remove('has-icon');
                }
            } else {
                // Fallback: construct URL from filename
                // This handles cases where MediaWiki might not have rendered the file yet
                const encodedFilename = encodeURIComponent(iconFile.replace(/ /g, '_'));
                const fallbackUrl = '/images/' + encodedFilename;
                
                // Check if the file exists by creating a test image
                const testImg = new Image();
                testImg.onload = function() {
                    card.style.backgroundImage = 'url(' + fallbackUrl + ')';
                    card.classList.add('has-background');
                    card.classList.remove('has-icon');
                };
                testImg.onerror = function() {
                    // If direct path fails, try the thumb path
                    const thumbUrl = '/images/thumb/' + encodedFilename + '/300px-' + encodedFilename;
                    card.style.backgroundImage = 'url(' + thumbUrl + ')';
                    card.classList.add('has-background');
                    card.classList.remove('has-icon');
                };
                testImg.src = fallbackUrl;
            }
        });
    }
    
    /**
     * Initialize when DOM is ready
     */
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', applyJourneyBackgrounds);
    } else {
        // DOM is already loaded
        applyJourneyBackgrounds();
    }
    
    // Also run after a short delay to catch any lazy-loaded images
    setTimeout(applyJourneyBackgrounds, 1000);
    
    // Make function available globally for debugging
    window.applyJourneyBackgrounds = applyJourneyBackgrounds;
})();

	// ……………………………………………………………………………………………………………………………

}( mediaWiki, jQuery ) );