|
|
| Line 392: |
Line 392: |
| }); | | }); |
|
| |
|
| /**
| |
| * Journey System JavaScript for MediaWiki
| |
| * Enhanced with proper task counting and initialization
| |
| */
| |
| (function($, mw) {
| |
| '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();
| |
| });
| |
|
| |
| })(jQuery, mediaWiki);
| |
|
| |
| /**
| |
| * 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:Common.js - Fixed Header Spacing
FIXED VERSION - Addresses all positioning issues
======================================== */
/* util: "Fabricator Basics" → "fabricator-basics" */
function makeSlug(str){
return str.toLowerCase().trim()
.replace(/[^\\w\\s-]/g,'') // drop punctuation
.replace(/\\s+/g,'-'); // spaces → dashes
}
$(document).ready(function() {
// FIXED: Check if page has breadcrumb navigation
var hasBreadcrumb = $('.dune-breadcrumb-nav').length > 0;
if (hasBreadcrumb) {
$('body').addClass('has-breadcrumb');
}
// HEADER FIX - Wait for page to load then force header
setTimeout(function() {
// Remove ALL existing headers and nav attempts
$('.mw-header, #mw-header-container, nav.tab-bar, .tab-bar').remove();
// Hide the page title and extra elements completely
$('.firstHeading, h1.title, .mw-page-title-main, #firstHeading, #tagline, h3#tagline, #contentSub, .mw-content-subtitle').remove();
// Remove any MediaWiki notices that might appear
$('#siteNotice, .mw-indicators, .printfooter, .catlinks').remove();
// Create the navigation bar HTML with inline styles
var headerHTML = `
<div id="dune-header" style="position: fixed; top: 0; left: 0; right: 0; height: 60px; background: linear-gradient(180deg, rgba(20, 18, 28, 0.95) 0%, rgba(10, 8, 16, 0.98) 100%); border-bottom: 3px solid rgba(252, 231, 200, 0.4); box-shadow: 0 2px 20px rgba(0,0,0,0.8); z-index: 99999; display: flex; align-items: center;">
<div style="width: 100%; max-width: 1600px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; padding: 0 20px; height: 100%;">
<a href="/wiki/Main_Page" style="font-family: 'Orbitron', sans-serif; font-size: 22px; color: #fce7c8; text-transform: uppercase; letter-spacing: 3px; font-weight: 700; text-shadow: 0 0 15px rgba(252, 231, 200, 0.4); text-decoration: none; white-space: nowrap;">DUNE DB</a>
<nav style="display: flex; gap: 0; margin: 0 20px; height: 100%;">
<a href="/wiki/Items" style="display: flex; align-items: center; padding: 0 18px; height: 100%; color: rgba(252, 231, 200, 0.8); font-family: 'Rajdhani', sans-serif; font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.2px; text-decoration: none; border-left: 1px solid rgba(252, 231, 200, 0.1); border-right: 1px solid rgba(252, 231, 200, 0.1); transition: all 0.3s ease; background: transparent;" onmouseover="this.style.background='rgba(252, 231, 200, 0.1)'; this.style.color='#fce7c8';" onmouseout="this.style.background='transparent'; this.style.color='rgba(252, 231, 200, 0.8)';">◇ Items</a>
<a href="/wiki/Crafting" style="display: flex; align-items: center; padding: 0 18px; height: 100%; color: rgba(252, 231, 200, 0.8); font-family: 'Rajdhani', sans-serif; font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.2px; text-decoration: none; border-right: 1px solid rgba(252, 231, 200, 0.1); transition: all 0.3s ease; background: transparent;" onmouseover="this.style.background='rgba(252, 231, 200, 0.1)'; this.style.color='#fce7c8';" onmouseout="this.style.background='transparent'; this.style.color='rgba(252, 231, 200, 0.8)';">⚒ Crafting</a>
<a href="/wiki/Building" style="display: flex; align-items: center; padding: 0 18px; height: 100%; color: rgba(252, 231, 200, 0.8); font-family: 'Rajdhani', sans-serif; font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.2px; text-decoration: none; border-right: 1px solid rgba(252, 231, 200, 0.1); transition: all 0.3s ease; background: transparent;" onmouseover="this.style.background='rgba(252, 231, 200, 0.1)'; this.style.color='#fce7c8';" onmouseout="this.style.background='transparent'; this.style.color='rgba(252, 231, 200, 0.8)';">⌂ Building</a>
<a href="/wiki/Tech_Tree" style="display: flex; align-items: center; padding: 0 18px; height: 100%; color: rgba(252, 231, 200, 0.8); font-family: 'Rajdhani', sans-serif; font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.2px; text-decoration: none; border-right: 1px solid rgba(252, 231, 200, 0.1); transition: all 0.3s ease; background: transparent;" onmouseover="this.style.background='rgba(252, 231, 200, 0.1)'; this.style.color='#fce7c8';" onmouseout="this.style.background='transparent'; this.style.color='rgba(252, 231, 200, 0.8)';">⬢ Tech Tree</a>
<a href="/wiki/Journeys" style="display: flex; align-items: center; padding: 0 18px; height: 100%; color: rgba(252, 231, 200, 0.8); font-family: 'Rajdhani', sans-serif; font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.2px; text-decoration: none; border-right: 1px solid rgba(252, 231, 200, 0.1); transition: all 0.3s ease; background: transparent;" onmouseover="this.style.background='rgba(252, 231, 200, 0.1)'; this.style.color='#fce7c8';" onmouseout="this.style.background='transparent'; this.style.color='rgba(252, 231, 200, 0.8)';">➤ Journeys</a>
<a href="/wiki/Skills" style="display: flex; align-items: center; padding: 0 18px; height: 100%; color: rgba(252, 231, 200, 0.8); font-family: 'Rajdhani', sans-serif; font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.2px; text-decoration: none; border-right: 1px solid rgba(252, 231, 200, 0.1); transition: all 0.3s ease; background: transparent;" onmouseover="this.style.background='rgba(252, 231, 200, 0.1)'; this.style.color='#fce7c8';" onmouseout="this.style.background='transparent'; this.style.color='rgba(252, 231, 200, 0.8)';">★ Skills</a>
<a href="/wiki/Map" style="display: flex; align-items: center; padding: 0 18px; height: 100%; color: rgba(252, 231, 200, 0.8); font-family: 'Rajdhani', sans-serif; font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.2px; text-decoration: none; border-right: 1px solid rgba(252, 231, 200, 0.1); transition: all 0.3s ease; background: transparent;" onmouseover="this.style.background='rgba(252, 231, 200, 0.1)'; this.style.color='#fce7c8';" onmouseout="this.style.background='transparent'; this.style.color='rgba(252, 231, 200, 0.8)';">🗺 Map</a>
</nav>
<div style="display: flex; align-items: center;">
<form action="/index.php" method="get" style="display: flex; align-items: center; gap: 0; height: 38px;">
<input type="search" name="search" placeholder="Search wiki..." style="background: rgba(0, 0, 2, 0.8); border: 2px solid rgba(252, 231, 200, 0.3); border-radius: 0; color: #fce7c8; padding: 0 12px; width: 200px; font-size: 13px; font-family: 'Rajdhani', sans-serif; transition: all 0.3s ease; outline: none; height: 38px; box-sizing: border-box; margin: 0; vertical-align: middle;" onfocus="this.style.borderColor='#fce7c8'; this.style.boxShadow='0 0 10px rgba(252, 231, 200, 0.3)'; this.style.width='250px';" onblur="this.style.borderColor='rgba(252, 231, 200, 0.3)'; this.style.boxShadow='none'; this.style.width='200px';">
<button type="submit" style="background: rgba(252, 231, 200, 0.1); border: 2px solid rgba(252, 231, 200, 0.3); border-left: none; color: #fce7c8; padding: 0 12px; cursor: pointer; font-size: 14px; transition: all 0.3s ease; height: 38px; box-sizing: border-box; margin: 0; vertical-align: middle; display: flex; align-items: center; justify-content: center;" onmouseover="this.style.background='rgba(252, 231, 200, 0.2)'; this.style.borderColor='#fce7c8';" onmouseout="this.style.background='rgba(252, 231, 200, 0.1)'; this.style.borderColor='rgba(252, 231, 200, 0.3)';"><span>🔍</span></button>
</form>
</div>
</div>
</div>
`;
// Check if header already exists to avoid duplicates
if ($('#dune-header').length === 0) {
$('body').prepend(headerHTML);
}
// CRITICAL: Force proper spacing
$('body').css({
'padding-top': '60px',
'margin-top': '0',
'overflow-y': 'auto',
'height': 'auto'
});
// FIXED: Different spacing for pages with/without breadcrumb
if (hasBreadcrumb) {
// Pages with breadcrumb - content starts immediately after header
$('#content, .mw-body, #mw-content-text, .mw-body-content, .mw-parser-output, #main-section, .main-section, #page-content').css({
'padding-top': '0',
'margin-top': '0'
});
// Breadcrumb should have proper spacing from header
$('.dune-breadcrumb-nav').css({
'margin-top': '20px',
'margin-bottom': '20px'
});
} else {
// Pages without breadcrumb - add padding to content
$('#content, .mw-body, #mw-content-text, .mw-body-content, .mw-parser-output, #main-section, .main-section, #page-content').css({
'padding-top': '20px',
'margin-top': '0'
});
}
// Force first element to start at top (unless it's breadcrumb)
$('.mw-parser-output > *:first-child:not(.dune-breadcrumb-nav), .mw-body-content > *:first-child:not(.dune-breadcrumb-nav)').css({
'margin-top': '0',
'padding-top': '0'
});
// Remove sidebar space
$('#sidebar, .columns.large-2.medium-3').css('display', 'none');
$('#page-content, .columns.large-10.medium-9').css({
'width': '100%',
'max-width': '100%',
'margin': '0',
'padding': '0'
});
// Handle Actions button positioning
$('#p-cactions').css({
'position': 'fixed',
'top': '11px',
'right': '20px',
'z-index': '100001',
'margin': '0',
'padding': '0',
'height': '38px'
});
// Remove spacing from all column wrappers
$('.row > .columns').css({
'padding-top': '0',
'margin-top': '0'
});
// FIXED: Ensure page content can scroll
$('#content').css({
'min-height': 'calc(100vh - 60px)',
'position': 'relative',
'z-index': '1'
});
// Remove any floating MediaWiki elements
$('.mw-jump-link, .mw-redirectedfrom, #siteNotice').remove();
}, 100);
});
/* ===== RADIAL MENU CODE ===== */
function waitForRadialData(callback, attempts = 0) {
const dataTag = document.getElementById('radialMenuData');
if (dataTag) {
try {
const menuItems = JSON.parse(dataTag.innerText || dataTag.textContent);
console.log("✅ Loaded radialMenuData:", menuItems);
callback(menuItems);
} catch (e) {
console.error("❌ Failed to parse radialMenuData", e);
}
} else if (attempts < 20) {
setTimeout(() => waitForRadialData(callback, attempts + 1), 100);
} else {
console.warn("⚠️ radialMenuData tag not found after timeout.");
}
}
$(document).ready(function () {
waitForRadialData(function (menuItems) {
const isMobile = () => window.innerWidth <= 768;
let radialItemsHTML = '';
menuItems.forEach((item, index) => {
const id = item.category.toLowerCase().replace(/\s+/g, '-');
item.id = id;
radialItemsHTML += `
<div class="dune-radial-item-container ${item.position}">
<a data-id="${id}" class="dune-radial-item">
<img src="${item.icon}" alt="${item.name}" class="dune-radial-icon">
<span class="dune-radial-tooltip">${item.name}</span>
</a>
</div>
`;
});
const centerButtonHTML = `
<a href="https://dunedb.com/Main_Page" class="dune-radial-center">
<img src="https://dunedb.com/images/9/99/HomeNavIcon.png" alt="Home" class="dune-radial-icon">
<span class="dune-radial-tooltip">Main Page</span>
</a>
`;
const radialMenuHTML = `
<div id="duneRadialMenu" class="dune-radial-menu">
<div class="dune-radial-background">
<div class="dune-radial-circle outer"></div>
<div class="dune-radial-circle middle"></div>
<div class="dune-radial-circle inner"></div>
</div>
${centerButtonHTML}
${radialItemsHTML}
</div>
<div id="duneRadialOverlay" class="dune-radial-overlay"></div>
`;
const subcategoryContainerHTML = `<div id="duneSubcategoryContainer" class="dune-subcategory-container"></div>`;
$('body').append(radialMenuHTML);
$('body').append(subcategoryContainerHTML);
if (isMobile()) {
$('#duneRadialMenu').addClass('mobile-grid');
}
const showSubcategories = (itemId, event) => {
const item = menuItems.find(i => i.id === itemId);
if (!item || !item.subcategories) return;
$('.dune-radial-item').removeClass('selected');
$(`.dune-radial-item[data-id="${itemId}"]`).addClass('selected');
let subcategoryHTML = `
<div class="dune-subcategory-header">
<img src="${item.icon}" alt="${item.name}" class="dune-subcategory-icon">
<span>${item.name}</span>
</div>
<div class="dune-subcategory-items">
`;
item.subcategories.forEach((sub, index) => {
subcategoryHTML += `
<a href="${sub.url}" class="dune-subcategory-item" style="--item-index: ${index}">
<span class="dune-subcategory-name">${sub.name}</span>
</a>
`;
});
subcategoryHTML += `
</div>
<div class="dune-subcategory-footer">
<a href="${item.url}" class="dune-subcategory-all">View All ${item.name}</a>
</div>
`;
const $subcategoryContainer = $('#duneSubcategoryContainer');
$subcategoryContainer.html(subcategoryHTML);
$subcategoryContainer.removeClass((index, className) => {
return (className.match(/from-\S+/g) || []).join(' ');
});
$subcategoryContainer.addClass(`from-${item.position}`);
$subcategoryContainer.addClass('active');
event.stopPropagation();
};
const hideSubcategories = () => {
$('#duneSubcategoryContainer').removeClass('active');
$('.dune-radial-item').removeClass('selected');
};
const toggleRadialMenu = () => {
if ($('#duneRadialMenu').hasClass('active')) {
hideSubcategories();
$('#duneRadialMenu').removeClass('active');
$('#duneRadialOverlay').removeClass('active');
} else {
$('#duneRadialMenu').addClass('active');
$('#duneRadialOverlay').addClass('active');
$('.dune-radial-item-container').each(function (index) {
const $item = $(this);
setTimeout(() => {
$item.addClass('animated');
}, index * 50);
});
setTimeout(() => {
$('.dune-radial-center').addClass('animated');
}, menuItems.length * 50);
}
};
$(document).on('click', '#duneLogoBtn, #menuRadialTrigger img', function (e) {
console.log("✅ Radial trigger clicked");
e.preventDefault();
e.stopPropagation();
toggleRadialMenu();
});
$(document).on('click', '.dune-radial-item', function (e) {
e.preventDefault();
e.stopPropagation();
const itemId = $(this).data('id');
if ($(this).hasClass('selected')) {
hideSubcategories();
} else {
showSubcategories(itemId, e);
}
});
$(document).on('click', '#duneRadialOverlay', function () {
hideSubcategories();
$('#duneRadialMenu').removeClass('active');
$('#duneRadialOverlay').removeClass('active');
});
$(document).on('click', 'li.name.logo a[href="/Main_Page"]', function (e) {
e.preventDefault();
toggleRadialMenu();
});
$(document).on('keydown', function (e) {
if (e.key === 'Escape') {
if ($('#duneSubcategoryContainer').hasClass('active')) {
hideSubcategories();
} else if ($('#duneRadialMenu').hasClass('active')) {
$('#duneRadialMenu').removeClass('active');
$('#duneRadialOverlay').removeClass('active');
}
}
});
$(window).on('resize', function () {
if (isMobile()) {
$('#duneRadialMenu').addClass('mobile-grid');
} else {
$('#duneRadialMenu').removeClass('mobile-grid');
}
});
});
});
/* Used-In dynamic filters & search */
mw.hook( 'wikipage.content' ).add( function ( $content ) {
const $holder = $content.find( '#usedInFilterHolder' );
const $table = $content.find( '#usedInTable' );
if ( !$holder.length || !$table.length ) return;
const rowsData = [];
const stations = new Set();
const types = new Set();
$table.find( 'tbody tr' ).each( function () {
const $tds = $( this ).children( 'td' );
const output = $tds.eq( 0 ).text().trim();
const type = $tds.eq( 1 ).text().trim();
const station = $tds.last().text().trim();
rowsData.push( { tr: this, output, type, station } );
stations.add( station );
types.add( type );
} );
const makeSel = ( id, label, opts ) => {
const $sel = $( '<select>', { id, class: 'game-button', css:{margin:'6px 4px 6px 0'} } )
.append( $( '<option>', { value:'', text:label } ) );
opts.forEach( o => $sel.append( $( '<option>', { value:o, text:o } ) ) );
return $sel;
};
const $stationSel = makeSel( 'filterStation', 'All Stations', [ ...stations ].sort() );
const $typeSel = makeSel( 'filterType', 'All Types', [ ...types ].sort() );
const $search = $( '<input>', {
type:'text',
placeholder:'Search…',
css:{ margin:'6px 0', padding:'4px', width:'140px',
'background':'#111', color:'#fff', border:'1px solid #555' }
} );
$holder.append( $stationSel, $typeSel, $search );
function applyFilters() {
const sVal = $stationSel.val();
const tVal = $typeSel.val();
const q = $search.val().toLowerCase();
rowsData.forEach( ({ tr, output, type, station }) => {
const show =
(!sVal || station === sVal) &&
(!tVal || type === tVal) &&
(!q || output.toLowerCase().includes( q ));
tr.style.display = show ? '' : 'none';
} );
}
$stationSel.on( 'change', applyFilters );
$typeSel .on( 'change', applyFilters );
$search .on( 'input', applyFilters );
} );
/* Lazy-load background image Blur */
document.addEventListener('DOMContentLoaded', () => {
const hero = document.querySelector('.dune-hero');
if (hero && !hero.style.backgroundImage) {
hero.style.backgroundImage =
'url(/images/7/70/Arrakis_dunes.jpg?2025)';
}
const radial = document.querySelector('.radial-menu-wrapper');
if (!radial) return;
const y = radial.offsetTop;
window.addEventListener('scroll', () => {
radial.classList.toggle('is-fixed', window.scrollY > y);
});
});
/**
* Resource Page Module Loader
*/
$(function() {
// Only on resource pages
if ($('.crafting-section, .recipe-table, [class*="crafted-"]').length > 0) {
// Load dependencies first
mw.loader.using(['jquery', 'mediawiki.api', 'mediawiki.util'], function() {
// Load resource page module
$.getScript('/index.php?title=MediaWiki:ResourcePage.js&action=raw&ctype=text/javascript')
.done(function() {
console.log('Resource page module loaded');
})
.fail(function() {
console.error('Failed to load resource page module');
});
});
}
});
$(function () {
const $drop = $('#drop');
const $menu = $('#drop1');
if ($drop.length && $menu.length) {
// Move Actions button into the footer visually
$('#footer').append($drop);
$('#footer').append($menu);
// Style the button like a footer element
$drop.css({
position: 'relative',
display: 'inline-block',
marginTop: '12px',
background: 'rgba(252,231,200,0.08)',
border: '1px solid rgba(252,231,200,0.2)',
color: '#fce7c8',
fontFamily: 'Rajdhani, sans-serif',
padding: '6px 12px',
boxShadow: '0 0 6px rgba(0,0,0,0.3)',
cursor: 'pointer'
});
$drop.hover(
() => $drop.css({ background: 'rgba(252,231,200,0.15)', color: '#fff' }),
() => $drop.css({ background: 'rgba(252,231,200,0.08)', color: '#fce7c8' })
);
// Ensure dropdown menu opens below and is styled cleanly
$menu.css({
position: 'absolute',
top: '100%',
right: '0',
zIndex: '9999',
background: '#1a1a1a',
boxShadow: '0 2px 6px rgba(0,0,0,0.5)',
marginTop: '4px'
});
}
// Footer message logic
if (!$('#custom-footer-note').length) {
const $customLine = $('<div>', {
id: 'custom-footer-note',
text: 'This is an experimental solo project exploring what’s possible in gaming UI and data experiences. Things may break—thank you for being part of the push forward.'
});
$('#footer').prepend($customLine);
$('#footer-left').hide();
}
});