1/** 2 * Compare Units Functionality 3 * Allows users to compare up to 3 units side by side 4 */ 5 6$(document).ready(function() { 7 // Compare functionality object 8 var Compare = { 9 units: [], 10 maxUnits: 3, 11 12 // Initialize compare functionality 13 init: function() { 14 this.bindEvents(); 15 this.loadFromStorage(); 16 }, 17 18 // Bind event handlers 19 bindEvents: function() { 20 var self = this; 21 22 // Compare button clicks 23 $(document).on('click', '.btn-compare', function(e) { 24 e.preventDefault(); 25 var unitId = $(this).data('unit-id'); 26 27 if ($(this).hasClass('added')) { 28 self.removeUnit(unitId); 29 } else { 30 var unitData = { 31 id: unitId, 32 stock: $(this).data('unit-stock') || '', 33 year: $(this).data('unit-year') || '', 34 make: $(this).data('unit-make') || '', 35 model: $(this).data('unit-model') || '' 36 }; 37 self.addUnit(unitData); 38 } 39 }); 40 41 // Banner toggle 42 $(document).on('click', '#compare-toggle', function() { 43 self.toggleBanner(); 44 }); 45 46 // Clear all button 47 $(document).on('click', '#clear-compare', function() { 48 self.clearAll(); 49 }); 50 51 // Compare button in banner 52 $(document).on('click', '#compare-units', function() { 53 self.showComparisonModal(); 54 }); 55 56 // Remove unit from banner 57 $(document).on('click', '.compare-unit-remove', function() { 58 var unitId = $(this).data('unit-id'); 59 self.removeUnit(unitId); 60 }); 61 }, 62 63 // Add unit to comparison 64 addUnit: function(unitData) { 65 if (this.units.length >= this.maxUnits) { 66 alert('You can only compare up to ' + this.maxUnits + ' units at a time.'); 67 return; 68 } 69 70 // Check if unit already exists 71 var exists = this.units.find(function(unit) { 72 return unit.id === unitData.id; 73 }); 74 75 if (exists) { 76 return; 77 } 78 79 // Store basic unit data for banner display 80 var unitToStore = { 81 id: unitData.id, 82 stock: unitData.stock || '', 83 year: unitData.year || '', 84 make: unitData.make || '', 85 model: unitData.model || '', 86 // These will be populated from AJAX response later 87 image_url: null, 88 unit_name: null 89 }; 90 91 this.units.push(unitToStore); 92 this.updateUI(); 93 this.saveToStorage(); 94 this.showBanner(); 95 96 // Fetch unit image immediately when adding 97 this.fetchUnitImages(); 98 99 // Update button state 100 $('.btn-compare[data-unit-id="' + unitData.id + '"]').addClass('added'); 101 }, 102 103 // Remove unit from comparison 104 removeUnit: function(unitId) { 105 this.units = this.units.filter(function(unit) { 106 return unit.id !== unitId; 107 }); 108 109 this.updateUI(); 110 this.saveToStorage(); 111 112 // Update button state 113 $('.btn-compare[data-unit-id="' + unitId + '"]').removeClass('added'); 114 115 // Hide banner if no units 116 if (this.units.length === 0) { 117 this.hideBanner(); 118 } 119 }, 120 121 // Clear all units 122 clearAll: function() { 123 this.units = []; 124 this.updateUI(); 125 this.saveToStorage(); 126 this.hideBanner(); 127 128 // Reset all button states 129 $('.btn-compare').removeClass('added'); 130 }, 131 132 // Update UI elements 133 updateUI: function() { 134 var count = this.units.length; 135 $('#compare-count-text').text('Compare (' + count + '/' + this.maxUnits + ')'); 136 137 // Enable/disable compare button 138 if (count >= 2) { 139 $('#compare-units').prop('disabled', false); 140 } else { 141 $('#compare-units').prop('disabled', true); 142 } 143 144 // Update units list in banner 145 this.updateUnitsList(); 146 }, 147 148 // Update the units list in banner 149 updateUnitsList: function() { 150 var $list = $('#compare-units-list'); 151 $list.empty(); 152
153 this.units.forEach(function(unit) { 154 var imageHtml = ''; 155 if (unit.image_url) { 156 imageHtml = '<img src="' + unit.image_url + '" alt="Unit Image" style="width:100%;height:100px;object-fit:cover;border-radius:3px;margin-bottom:8px;" />'; 157 } else { 158 imageHtml = '<div class="unit-placeholder-img" style="width:100%;height:100px;background:#f0f0f0;border-radius:3px;margin-bottom:8px;display:flex;align-items:center;justify-content:center;color:#999;">Image</div>'; 159 } 160 161 var unitTitle = unit.unit_name || (unit.year + ' ' + unit.make + ' ' + unit.model); 162 var stockText = unit.stock ? 'Stock: ' + unit.stock : ''; 163 164 var unitHtml = '<div class="compare-unit-item">' + 165 '<button class="compare-unit-remove" data-unit-id="' + unit.id + '">×</button>' + 166 imageHtml + 167 '<h4>' + unitTitle + '</h4>' + 168 '<div class="unit-details">' + stockText + '</div>' + 169 '</div>'; 170 $list.append(unitHtml); 171 }); 172 }, 173 174 // Show comparison banner 175 showBanner: function() { 176 $('#compare-banner').removeClass('hidden').addClass('show'); 177 }, 178 179 // Hide comparison banner 180 hideBanner: function() { 181 $('#compare-banner').removeClass('show expanded').addClass('hidden'); 182 }, 183 184 // Toggle banner expanded state 185 toggleBanner: function() { 186 $('#compare-banner').toggleClass('expanded'); 187 188 // Images are now fetched immediately when units are added, 189 // so no need to fetch again on banner expand 190 }, 191 192 // Fetch unit images and details for banner display 193 fetchUnitImages: function() { 194 var self = this; 195 var unitsNeedingData = this.units.filter(function(unit) { 196 return !unit.image_url; // Only fetch data for units we don't have images for 197 }); 198 199 if (unitsNeedingData.length === 0) { 200 return; // All units already have data 201 } 202 203 var unitIds = unitsNeedingData.map(function(unit) { 204 return unit.id; 205 }); 206 207 // Make AJAX request to get basic unit data (reuse compare endpoint) 208 $.ajax({ 209 url: WS_PATH + "modules/unit/includes/compare_ajax.php?r=" + Math.random(), 210 type: 'POST', 211 data: { 212 do_what: 'get_comparison_data', 213 unit_ids: JSON.stringify(unitIds) 214 }, 215 success: function(response) { 216 // Handle different response formats 217 if (typeof response === 'string') { 218 var parts = response.split('##'); 219 if (parts[0] === '0') { 220 return; // Error, but don't break the banner 221 } 222 try { 223 response = JSON.parse(parts[1] || response); 224 } catch (e) { 225 return; // Invalid response, but don't break the banner 226 } 227 } 228 229 if (response && response.status === 'success' && response.units) { 230 // Update our stored units with the fetched data 231 response.units.forEach(function(fetchedUnit) { 232 var storedUnit = self.units.find(function(unit) { 233 return unit.id == fetchedUnit.unit_id; 234 }); 235 if (storedUnit) { 236 storedUnit.image_url = fetchedUnit.image_url; 237 storedUnit.unit_name = fetchedUnit.unit_name; 238 } 239 }); 240
241 // Update the banner display with new images 242 self.updateUnitsList(); 243 self.saveToStorage(); // Save the updated data 244 } 245 }, 246 error: function() { 247 // Silently fail - banner will just show placeholder images 248 } 249 }); 250 }, 251 252 // Show comparison modal 253 showComparisonModal: function() { 254 if (this.units.length < 2) { 255 alert('Please select at least 2 units to compare.'); 256 return; 257 } 258 259 this.loadComparisonData(); 260 }, 261 262 // Load comparison data and show modal 263 loadComparisonData: function() { 264 var self = this; 265 var modalBody = $('#compare-modal-body'); 266 267 // Show loading message 268 modalBody.html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> Loading comparison data...</div>'); 269 $('#compare-modal').modal('show'); 270 271 // Get unit IDs for AJAX call 272 var unitIds = this.units.map(function(unit) { 273 return unit.id; 274 }); 275 276 // Make AJAX request to fetch comparison data 277 $.ajax({ 278 url: WS_PATH + "modules/unit/includes/compare_ajax.php?r=" + Math.random(), 279 type: 'POST', 280 data: { 281 do_what: 'get_comparison_data', 282 unit_ids: JSON.stringify(unitIds) 283 }, 284 success: function(response) { 285 // Handle different response formats 286 if (typeof response === 'string') { 287 // Handle legacy format: "0##Error message" or "1##Success message" 288 var parts = response.split('##'); 289 if (parts[0] === '0') { 290 modalBody.html('<div class="alert alert-danger">' + (parts[1] || 'Error loading comparison data.') + '</div>'); 291 return; 292 } 293 // If it's a string but starts with 1, try to parse as JSON 294 try { 295 response = JSON.parse(parts[1] || response); 296 } catch (e) { 297 modalBody.html('<div class="alert alert-danger">Invalid response format.</div>'); 298 return; 299 } 300 } 301 302 if (response && response.status === 'success') { 303 self.renderComparisonTable(response); 304 } else { 305 modalBody.html('<div class="alert alert-danger">Error loading comparison data. Please try again.</div>'); 306 } 307 }, 308 error: function(xhr, textStatus, error) { 309 console.error('AJAX Error:', error, xhr.responseText); 310 modalBody.html('<div class="alert alert-danger">Error loading comparison data. Please try again.</div>'); 311 } 312 }); 313 }, 314 315 // Render comparison table 316 renderComparisonTable: function(response) { 317 var modalBody = $('#compare-modal-body'); 318 319 if (!response || !response.units || response.units.length === 0) { 320 modalBody.html('<div class="alert alert-danger">No unit data available for comparison.</div>'); 321 return; 322 } 323 324 var units = response.units; 325 var specs = response.specs || []; 326 var unitCount = units.length; 327 328 // Create mobile-friendly tabbed interface 329 var mobileHtml = this.renderMobileComparison(units, specs); 330 var desktopHtml = this.renderDesktopComparison(units, specs, unitCount); 331 332 modalBody.html( 333 '<div class="compare-mobile-view">' + mobileHtml + '</div>' + 334 '<div class="compare-desktop-view">
334' + desktopHtml + '</div>' 335 ); 336 337 // Initialize mobile functionality 338 this.initializeMobileComparison(units.length); 339 }, 340 341 // Render mobile tabbed comparison 342 renderMobileComparison: function(units, specs) { 343 var self = this; 344 var html = ''; 345 346 // Tab navigation 347 html += '<div class="compare-mobile-tabs">'; 348 units.forEach(function(unit, index) { 349 var activeClass = index === 0 ? ' active' : ''; 350 var unitTitle = unit.unit_name || (unit.model_year + ' ' + unit.make + ' ' + unit.model); 351 html += '<div class="compare-tab' + activeClass + '" data-tab="' + index + '">' + 352 '<span class="tab-title">' + unitTitle + '</span>' + 353 '</div>'; 354 }); 355 html += '</div>'; 356 357 // Filter basic specs to only include those where ALL units have values 358 var basicSpecs = [ 359 { label: 'Unit Name', field: 'unit_name' }, 360 { label: 'Model Year', field: 'model_year' }, 361 { label: 'Retail Price', field: 'retail_price_display' }, 362 { label: 'Sales Price', field: 'sales_price_display' } 363 ]; 364 365 var filteredBasicSpecs = basicSpecs.filter(function(spec) { 366 return units.every(function(unit) { 367 var value = unit[spec.field] || ''; 368 if (spec.field.includes('price_display') && (!value || value === '')) { 369 return false; 370 } 371 return value && value !== '' && value !== 'N/A'; 372 }); 373 }); 374 375 // Filter custom specs to only include those where ALL units have values 376 var filteredSpecs = specs.filter(function(spec) { 377 return units.every(function(unit) { 378 var specData = unit.specs && unit.specs[spec.code]; 379 var value = specData ? specData.value : ''; 380 return value && value !== '' && value !== 'N/A'; 381 }); 382 }); 383 384 // Tab content 385 html += '<div class="compare-mobile-content">'; 386 units.forEach(function(unit, index) { 387 var activeClass = index === 0 ? ' active' : ''; 388 html += '<div class="compare-tab-content' + activeClass + '" data-content="' + index + '">'; 389 390 // Unit header with image 391 html += '<div class="mobile-unit-header">'; 392 if (unit.image_url) { 393 html += '<img src="' + unit.image_url + '" alt="' + unit.unit_name + '" class="mobile-unit-image" />'; 394 } 395 html += '<h3>' + (unit.unit_name || (unit.model_year + ' ' + unit.make + ' ' + unit.model)) + '</h3>'; 396 html += '</div>'; 397 398 // Specs table for this unit 399 html += '<div class="mobile-specs-table">'; 400 401 // Basic specs (only those where all units have values) 402 filteredBasicSpecs.forEach(function(spec) { 403 var value = unit[spec.field] || 'N/A'; 404 if (spec.field.includes('price_display') && (!value || value === '')) { 405 value = 'N/A'; 406 } 407 html += '<div class="mobile-spec-row">' + 408 '<div class="mobile-spec-label">' + spec.label + '</div>' + 409 '<div class="mobile-spec-value">' + value + '</div>' + 410 '</div>'; 411 }); 412 413 // Custom specs (only those where all units have values) 414 filteredSpecs.forEach(function(spec) { 415 var specData = unit.specs && unit.specs[spec.code]; 416 var value = specData ? specData.value : 'N/A'; 417 html += '<div class="mobile-spec-row">' + 418 '<div class="mobile-spec-label">' + spec.name + '</div>' + 419 '<div class="mobile-spec-value">' + value + '</div>' + 420 '</div>'; 421 }); 422 423 html += '</div>'; // mobile-specs-table 424 html += '</div>'; // compare-tab-content 425 }); 426 html += '</div>'; // compare-mobile-content 427 428 return html; 429 }, 430 431 // Render desktop table comparison 432 renderDesktopComparison: function(units, specs, unitCount) { 433 var tableHtml = '<table class="table compare-table units-' + unitCount + '">'; 434 435 // Header row with unit images and names (no "Unit" label) 436 tableHtml += '<tr class="unit-header-row">'; 437 tableHtml += '<td class="spec-label unit-header-spacer"></td>'; // Empty spacer cell
438 units.forEach(function(unit) { 439 tableHtml += '<td class="unit-column unit-header">'; 440 if (unit.image_url) { 441 tableHtml += '<img src="' + unit.image_url + '" alt="' + unit.unit_name + '" />'; 442 } else { 443 tableHtml += '<div class="unit-placeholder-img" style="width:100%;max-width:200px;height:150px;background:#f0f0f0;border-radius:5px;margin:0 auto 10px;display:flex;align-items:center;justify-content:center;color:#999;">No Image</div>'; 444 } 445 tableHtml += '<h4>' + (unit.unit_name || (unit.model_year + ' ' + unit.make + ' ' + unit.model)) + '</h4>'; 446 tableHtml += '</td>'; 447 }); 448 tableHtml += '</tr>'; 449 450 // Basic info rows (using the correct field names from ride_units) 451 var basicSpecs = [ 452 { label: 'Unit Name', field: 'unit_name' }, 453 { label: 'Model Year', field: 'model_year' }, 454 { label: 'Retail Price', field: 'retail_price_display' }, 455 { label: 'Sales Price', field: 'sales_price_display' } 456 ]; 457 458 // Filter basic specs - only show if ALL units have the value 459 basicSpecs.forEach(function(spec) { 460 var hasAllValues = true; 461 462 // Check if all units have this spec value 463 units.forEach(function(unit) { 464 var value = unit[spec.field] || ''; 465 466 // Handle empty price displays 467 if (spec.field.includes('price_display') && (!value || value === '')) { 468 hasAllValues = false; 469 } else if (!value || value === '' || value === 'N/A') { 470 hasAllValues = false; 471 } 472 }); 473 474 // Only add row if all units have values 475 if (hasAllValues) { 476 tableHtml += '<tr class="spec-row">'; 477 tableHtml += '<td class="spec-label">' + spec.label + '</td>'; 478 units.forEach(function(unit) { 479 var value = unit[spec.field] || 'N/A'; 480 481 // Handle empty price displays 482 if (spec.field.includes('price_display') && (!value || value === '')) { 483 value = 'N/A'; 484 } 485 486 tableHtml += '<td class="spec-value">' + value + '</td>'; 487 }); 488 tableHtml += '</tr>'; 489 } 490 }); 491 492 // Custom specification rows from database - only show if ALL units have the value 493 specs.forEach(function(spec) { 494 var hasAllValues = true; 495 496 // Check if all units have this spec value 497 units.forEach(function(unit) { 498 var specData = unit.specs && unit.specs[spec.code]; 499 var value = specData ? specData.value : ''; 500 if (!value || value === '' || value === 'N/A') { 501 hasAllValues = false; 502 } 503 }); 504 505 // Only add row if all units have values 506 if (hasAllValues) { 507 tableHtml += '<tr class="spec-row">'; 508 tableHtml += '<td class="spec-label">' + spec.name + '</td>'; // spec.name is the display label 509 units.forEach(function(unit) { 510 var specData = unit.specs && unit.specs[spec.code]; // spec.code is the field name 511 var value = specData ? specData.value : 'N/A'; 512 tableHtml += '<td class="spec-value">' + value + '</td>'; 513 }); 514 tableHtml += '</tr>'; 515 } 516 }); 517 518 tableHtml += '</table>'; 519 return tableHtml; 520 }, 521 522 // Initialize mobile comparison functionality 523 initializeMobileComparison: function(unitCount) { 524 var self = this; 525 526 // Initialize swipe tracking variables 527 this.swipeStartX = 0; 528 this.swipeCurrentX = 0; 529 this.isDragging = false;
530 531 // Tab click handlers 532 $(document).off('click', '.compare-tab').on('click', '.compare-tab', function() { 533 var tabIndex = parseInt($(this).data('tab')); 534 self.switchToTab(tabIndex); 535 }); 536 537 // Touch/swipe and mouse drag handlers 538 var contentArea = document.querySelector('.compare-mobile-content'); 539 if (contentArea) { 540 // Touch events for mobile 541 contentArea.addEventListener('touchstart', function(e) { 542 self.swipeStartX = e.touches[0].clientX; 543 self.isDragging = true; 544 }, { passive: true }); 545 546 contentArea.addEventListener('touchmove', function(e) { 547 if (!self.isDragging) return; 548 self.swipeCurrentX = e.touches[0].clientX; 549 }, { passive: true }); 550 551 contentArea.addEventListener('touchend', function() { 552 if (!self.isDragging) return; 553 self.isDragging = false; 554 self.handleSwipeEnd(unitCount); 555 }, { passive: true }); 556 557 // Mouse events for desktop testing 558 contentArea.addEventListener('mousedown', function(e) { 559 self.swipeStartX = e.clientX; 560 self.isDragging = true; 561 e.preventDefault(); // Prevent text selection 562 }); 563 564 contentArea.addEventListener('mousemove', function(e) { 565 if (!self.isDragging) return; 566 self.swipeCurrentX = e.clientX; 567 }); 568 569 contentArea.addEventListener('mouseup', function() { 570 if (!self.isDragging) return; 571 self.isDragging = false; 572 self.handleSwipeEnd(unitCount); 573 }); 574 575 // Handle mouse leaving the area 576 contentArea.addEventListener('mouseleave', function() { 577 if (!self.isDragging) return; 578 self.isDragging = false; 579 self.handleSwipeEnd(unitCount); 580 }); 581 } 582 }, 583 584 // Handle swipe/drag end logic 585 handleSwipeEnd: function(unitCount) { 586 var diffX = this.swipeStartX - this.swipeCurrentX; 587 var threshold = 50; // minimum swipe distance 588 589 if (Math.abs(diffX) > threshold) { 590 var currentTab = $('.compare-tab.active').data('tab'); 591 if (diffX > 0 && currentTab < unitCount - 1) { 592 // Swipe left - next tab 593 this.switchToTab(currentTab + 1); 594 } else if (diffX < 0 && currentTab > 0) { 595 // Swipe right - previous tab 596 this.switchToTab(currentTab - 1); 597 } 598 } 599 }, 600 601 // Switch to specific tab 602 switchToTab: function(tabIndex) { 603 // Update tab navigation 604 $('.compare-tab').removeClass('active'); 605 $('.compare-tab[data-tab="' + tabIndex + '"]').addClass('active'); 606 607 // Update content 608 $('.compare-tab-content').removeClass('active'); 609 $('.compare-tab-content[data-content="' + tabIndex + '"]').addClass('active'); 610 }, 611 612 // Save to localStorage 613 saveToStorage: function() { 614 localStorage.setItem('compareUnits', JSON.stringify(this.units)); 615 }, 616 617 // Load from localStorage 618 loadFromStorage: function() { 619 var stored = localStorage.getItem('compareUnits'); 620 if (stored) { 621 this.units = JSON.parse(stored); 622 if (this.units.length > 0) { 623 this.updateUI(); 624 this.showBanner(); 625 626 // Update button states
627 this.units.forEach(function(unit) { 628 $('.btn-compare[data-unit-id="' + unit.id + '"]').addClass('added'); 629 }); 630 } 631 } 632 } 633 }; 634 635 // Initialize compare functionality 636 Compare.init(); 637});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.