PageSourceSearch

https://powertoswitch.co.uk/wp-content/plugins/powertoswitch//assets/js/installer.js?ver=1788155000

js powertoswitch.co.uk collected 2026-09-30 16:34:51 UTC 32,513 bytes, 693 lines download raw bytes

1(function($) {
2    $(document).ready(function() {
3        function showPostcodeError(input, message) {
4            input.css('border', '1px solid #ff473e');
5            $('.installer-input-wrap').after(
6                '<div class="installer-error-msg" style="color: #ff473e; font-size: 13px;">' + message + '</div>'
7            );
8        }
9
10        var skeletonCardHTML = 
11            '<div class="p2s-installer-card">' +
12                '<div class="p2s-card-header">' +
13                    '<div class="p2s-skeleton" style="width: 50px; height: 50px; border-radius: 13px; flex-shrink: 0;"></div>' +
14                    '<div class="p2s-card-title-area" style="flex: 1;">' +
15                        '<div class="p2s-skeleton" style="height: 16px; width: 80%; margin-bottom: 8px; border-radius: 4px;"></div>' +
16                        '<div class="p2s-skeleton" style="height: 13px; width: 50%; border-radius: 4px;"></div>' +
17                    '</div>' +
18                '</div>' +
19                '<div class="p2s-card-rating">' +
20                    '<div class="p2s-skeleton" style="height: 14px; width: 40%; border-radius: 4px;"></div>' +
21                '</div>' +
22                '<div style="margin-top: 5px;">' +
23                    '<div class="p2s-skeleton" style="height: 13px; width: 95%; margin-bottom: 6px; border-radius: 4px;"></div>' +
24                    '<div class="p2s-skeleton" style="height: 13px; width: 75%; border-radius: 4px;"></div>' +
25                '</div>' +
26                '<div class="p2s-card-badges" style="margin-top: 4px;">' +
27                    '<div class="p2s-skeleton" style="height: 22px; width: 130px; border-radius: 7px;"></div>' +
28                    '<div class="p2s-skeleton" style="height: 22px; width: 110px; border-radius: 7px;"></div>' +
29                '</div>' +
30                '<div class="p2s-card-meta-bottom">' +
31                    '<div class="p2s-skeleton" style="height: 12px; width: 25%; border-radius: 4px;"></div>' +
32                    '<div class="p2s-skeleton" style="height: 12px; width: 25%; border-radius: 4px;"></div>' +
33                    '<div class="p2s-skeleton" style="height: 12px; width: 25%; border-radius: 4px;"></div>' +
34                '</div>' +
35                '<div class="p2s-card-actions">' +
36                    '<div class="p2s-skeleton" style="height: 38px; width: 55px; border-radius: 10px;"></div>' +
37                    '<div class="p2s-skeleton" style="height: 38px; flex: 1 1 0%; border-radius: 10px;"></div>' +
38                    '<div class="p2s-skeleton" style="height: 38px; flex: 1.3 1 0%; border-radius: 10px;"></div>' +
39                '</div>' +
40            '</div>';
41
42        function renderSkeletons(count) {
43            $('.p2s-empty-state').hide();
44            var html = '';
45            for (var i = 0; i < (count || 3); i++) {
46                html += skeletonCardHTML;
47            }
48            $('.p2s-installers-grid').html(html);
49        }
50
51        var savedLocation = localStorage.getItem('p2s_location');
52        if (savedLocation) {
53            $('.p2s-location-name').text(savedLocation);
54            $('input[name="installer-postcode"]').val(savedLocation);
55            $('.p2s-installer-step-one').hide();
56            $('.p2s-installer-step-two').show();
57            $('.p2s-controls-row, .p2s-filter-chips').show();
58            fetchInstallers();
59        }
60
61        $('.p2s-location-change').on('click', function() {
62            $('.p2s-location-text').hide();
63            $('.p2s-location-input').val(localStorage.getItem('p2s_location') || '');
64            $('.p2s-location-edit').css('display', 'flex');
65            $('.p2s-location-input').focus();
66        });
67
68        function saveInlineLocation() {
69            var val = $('.p2s-location-input').val().trim();
70            if (val === '') return;
71
72            var isStrictPostcode = /^BT[0-9]{1,2}\s?[0-9][a-zA-Z]{2}$/i.test(val);
73            var searchType = isStrictPostcode ? 'postcode' : 'city';
74
75            localStorage.setItem('p2s_location', val);
76            localStorage.setItem('p2s_search_type', searchType);
77
78            var endpoint = isStrictPostcode 
79                ? 'https://api.postcodes.io/postcodes/' + encodeURIComponent(val.replace(/\s+/g, ''))
80                : 'https://api.mapbox.com/geocoding/v5/mapbox.places/' + encodeURIComponent(val) + '.json?country=GB&types=place,locality,district&limit=5&access_token=' 
80+ p2s_installer_vars.mapbox_token;
81            
82            $.ajax({
83                url: endpoint,
84                type: 'GET',
85                success: function(res) {
86                    $('.installer-error-msg').remove();
87                    $('.p2s-location-input').css('border', '');
88                    var lat = null, lng = null;
89                    if (isStrictPostcode && res && res.result) {
90                        lat = res.result.latitude;
91                        lng = res.result.longitude;
92                    } else if (!isStrictPostcode && res && res.features && res.features.length > 0) {
93                        var match = res.features.find(function(f) {
94                            return f.text.toLowerCase() === val.toLowerCase();
95                        }) || res.features[0];
96                        lat = match.center[1];
97                        lng = match.center[0];
98                    }
99
100                    if (lat && lng) {
101                        localStorage.setItem('p2s_installer_lat', lat);
102                        localStorage.setItem('p2s_installer_lng', lng);
103                    } else {
104                        localStorage.removeItem('p2s_installer_lat');
105                        localStorage.removeItem('p2s_installer_lng');
106                        if (!isStrictPostcode) {
107                            $('.p2s-location-input').css('border', '1px solid red');
108                            $('.p2s-location-edit').after('<span class="installer-error-msg" style="color:red;font-size:13px;display:block;margin-top:4px;">Location not found. Please enter a valid town or postcode.</span>');
109                            return;
110                        }
111                    }
112                    finishInlineSave(val);
113                },
114                error: function() {
115                    $('.installer-error-msg').remove();
116                    $('.p2s-location-input').css('border', '1px solid red');
117                    $('.p2s-location-edit').after('<span class="installer-error-msg" style="color:red;font-size:13px;display:block;margin-top:4px;">Postcode not found. Please check and try again.</span>');
118                }
119            });
120
121            function finishInlineSave(loc) {
122                $('.p2s-location-name').text(loc);
123                $('.p2s-location-edit').hide();
124                $('.p2s-location-text').show();
125                fetchInstallers();
126            }
127        }
128
129        $('.p2s-location-save-btn').on('click', saveInlineLocation);
130
131        $('.p2s-location-input').on('keypress', function(e) {
132            if (e.which === 13) {
133                e.preventDefault();
134                saveInlineLocation();
135            }
136        });
137
138        $('.installer-step-one-btn').on('click', function(e) {
139            e.preventDefault();
140
141            var locationInput = $('input[name="installer-postcode"]');
142            var locationVal = locationInput.val().trim();
143
144            $('.installer-error-msg').remove();
145            locationInput.css('border', '');
146
147            var isStrictPostcode = /^BT[0-9]{1,2}\s?[0-9][a-zA-Z]{2}$/i.test(locationVal);
148            var containsNumber = /\d/.test(locationVal);
149
150            if (locationVal === '') {
151                return showPostcodeError(locationInput, 'Please enter a postcode or town.');
152            }
153            if (containsNumber && !isStrictPostcode) {
154                return showPostcodeError(locationInput, 'Please enter a valid Northern Ireland postcode (e.g. BT1 5GS).');
155            }
156            if (!containsNumber && locationVal.length < 3) {
157                return showPostcodeError(locationInput, 'Please enter a valid town name.');
158            }
159
160            var searchType = isStrictPostcode ? 'postcode' : 'city';
161
162            localStorage.setItem('p2s_location', locationVal);
163            localStorage.setItem('p2s_search_type', searchType);
164
165            var endpoint = isStrictPostcode 
166                ? 'https://api.postcodes.io/postcodes/' + encodeURIComponent(locationVal.replace(/\s+/g, ''))
167                : 'https://api.mapbox.com/geocoding/v5/mapbox.places/' + encodeURIComponent(locationVal) + '.json?country=GB&types=place,locality,district&limit=5&access_token=' 
167+ p2s_installer_vars.mapbox_token;
168
169            $.ajax({
170                url: endpoint,
171                type: 'GET',
172                success: function(res) {
173                    var lat = null, lng = null;
174                    if (isStrictPostcode && res && res.result) {
175                        lat = res.result.latitude;
176                        lng = res.result.longitude;
177                    } else if (!isStrictPostcode && res && res.features && res.features.length > 0) {
178                        var match = res.features.find(function(f) {
179                            return f.text.toLowerCase() === locationVal.toLowerCase();
180                        }) || res.features[0];
181                        lat = match.center[1];
182                        lng = match.center[0];
183                    }
184
185                    if (lat && lng) {
186                        localStorage.setItem('p2s_installer_lat', lat);
187                        localStorage.setItem('p2s_installer_lng', lng);
188                    } else {
189                        localStorage.removeItem('p2s_installer_lat');
190                        localStorage.removeItem('p2s_installer_lng');
191                        if (!isStrictPostcode) {
192                            return showPostcodeError(locationInput, 'Location not found. Please enter a valid town or postcode.');
193                        }
194                    }
195                    proceedToStepTwo(locationVal);
196                },
197                error: function() {
198                    showPostcodeError(locationInput, 'Postcode not found. Please check and try again.');
199                }
200            });
201
202            function proceedToStepTwo(loc) {
203                $('.p2s-location-name').text(loc);
204                $('.p2s-installer-step-one').hide();
205                $('.p2s-installer-step-two').show();
206                $('.p2s-controls-row, .p2s-filter-chips').show();
207                fetchInstallers();
208            }
209        });
210
211        var currentXhr = null;
212        var currentPage = 1;
213
214        $('.p2s-radius-select, .p2s-sort-select').on('change', function() {
215            currentPage = 1;
216            fetchInstallers();
217        });
218
219        $('.p2s-chip').on('click', function() {
220            if ($(this).hasClass('p2s-chip-active')) {
221                return;
222            }
223            $('.p2s-chip').removeClass('p2s-chip-active');
224            $(this).addClass('p2s-chip-active');
225            currentPage = 1;
226            fetchInstallers();
227        });
228
229        function fetchInstallers() {
230            if (currentXhr) {
231                currentXhr.abort();
232            }
233
234            renderSkeletons(6);
235            
236            currentXhr = $.ajax({
237                url: '/wp-admin/admin-ajax.php',
238                type: 'POST',
239                data: {
240                    action: 'p2s_get_installers',
241                    location: localStorage.getItem('p2s_location'),
242                    search_type: localStorage.getItem('p2s_search_type'),
243                    lat: localStorage.getItem('p2s_installer_lat'),
244                    lng: localStorage.getItem('p2s_installer_lng'),
245                    radius: $('.p2s-radius-select').val(),
246                    sort: $('.p2s-sort-select').val(),
247                    filter: $('.p2s-chip-active').text().trim(),
248                    saved_ids: localStorage.getItem('p2s_saved_installers') || '[]',
249                    page: currentPage
250                },
251                success: function(res) {
252                    if (res.success && res.data.html) {
253                        $('.p2s-installers-grid').html(res.data.html);
254                        $('.p2s-results-count span').text(res.data.count);
255                        $('.p2s-location-sub').show().find('.sc-interp').text(res.data.count);
256                        $('.p2s-installer-listing-tip').show();
257                        $('.p2s-empty-state').hide();
258                    } else {
259                        $('.p2s-installers-grid').empty();
260                        $('.p2s-installer-listing-tip').hide();
261                        $('.p2s-empty-state').show();
262                        $('.p2s-results-count span').text('0');
263                        $('.p2s-location-sub').show().find('.sc-interp').text('0');
264                    }
265                    if (res.success) {
266                        $('.p2s-installer-pagination').html(res.data.pagination_html || '');
267                    }
268                    renderCompareTray();
269                }
270            });
271        }
272
273        $(document).on('click', '.p2s-installer-page-btn, .p2s-installer-page-num', function() {
274            var $btn = $(this);
275            if ($btn.is('[disabled]') || $btn.hasClass('p2s-installer-page-active')) {
276                return;
277            }
278            currentPage = parseInt($btn.attr('data-page'), 10);
279            fetchInstallers();
280            $('html, body').animate({ scrollTop: $('.p2s-results-count').offset().top - 80 }, 300);
281        });
282
283        $(document).on('click', '.p2s-btn-quote', function(e) {
284            e.preventDefault();
285            var btn = $(this);
286            if (btn.hasClass('p2s-btn-disabled')) return;
287            var card = btn.closest('.p2s-installer-card');
288            var id = btn.attr('data-id');
289            var name = card.find('.p2s-card-title').text().trim();
290
291            var list = JSON.parse(localStorage.getItem('p2s_compare_list') || '[]');
292            var existingIndex = list.findIndex(function(item) { return String(item.id) === String(id); });
293
294            if (existingIndex !== -1) {
295                // Already added — remove from list
296                list.splice(existingIndex, 1);
297                localStorage.setItem('p2s_compare_list', JSON.stringify(list));
298                renderCompareTray();
299            } else if (list.length < 3) {
300                // Add to list
301                list.push({ id: id, name: name });
302                localStorage.setItem('p2s_compare_list', JSON.stringify(list));
303                renderCompareTray();
304            }
305        });
306
307
308        $(document).on('click', '.p2s-tray-remove-btn', function(e) {
309            e.preventDefault();
310            var name = $(this).attr('data-name');
311            var list = JSON.parse(localStorage.getItem('p2s_compare_list') || '[]');
312
313            list = list.filter(function(item) { return item.name !== name; });
314            localStorage.setItem('p2s_compare_list', JSON.stringify(list));
315
316            renderCompareTray();
317        });
318
319        $(document).on('click', '.p2s-tray-clear-btn', function(e) {
320            e.preventDefault();
321            localStorage.removeItem('p2s_compare_list');
322            renderCompareTray();
323        });
324
325        $(document).on('click', '.p2s-tray-quote-btn', function(e) {
326            e.preventDefault();
327            var list = JSON.parse(localStorage.getItem('p2s_compare_list') || '[]');
328            var ids = list.map(function(item) { return item.id; }).join(',');
329            window.location.href = '/find-an-installer/request-a-quote/' + (ids ? '?installer_ids=' + ids : '');
330        });
331
332        $(document).on('change', '.p2s-custom-quote-form label.acf-basic-uploader input[type="file"]', function() {
333            var fileName = this.files && this.files[0] ? this.files[0].name : '';
334            var $label = $(this).closest('label.acf-basic-uploader');
335            
336            if (fileName) {
337                $label.addClass('has-file');
338                if ($label.find('.p2s-file-name').length === 0) {
339                    $label.append('<span class="p2s-file-name"></span>');
340                }
341                $label.find('.p2s-file-name').text('📎 ' + fileName);
342            } else {
343                $label.removeClass('has-file');
344                $label.find('.p2s-file-name').remove();
345            }
346        });
347
348        var urlParams = new URLSearchParams(window.location.search);
349
350        if ($('.p2s-custom-quote-form').length) {
351            var urlInstallerIds = urlParams.get('installer_ids');
352            var urlInstallerId  = urlParams.get('installer_id');
353            var storedList = JSON.parse(localStorage.getItem('p2s_compare_list') || '[]');
354            var compareData;
355
356            if (urlInstallerIds) {
357                var idList = urlInstallerIds.split(',').map(function(id) {
358                    return { id: id.trim(), name: '' };
359                });
360                compareData = JSON.stringify(idList);
361                localStorage.setItem('p2s_compare_list', compareData);
362            } else if (urlInstallerId) {
363                var singleList = [{ id: urlInstallerId.trim(), name: '' }];
364                compareData = JSON.stringify(singleList);
365                localStorage.setItem('p2s_compare_list', compareData);
366            } else if (storedList.length > 0) {
367                compareData = JSON.stringify(storedList);
368            } else {
369                compareData = '[]';
370            }
371
372            $('.p2s-custom-quote-form').append(
373                '<input type="hidden" name="p2s_compare_list" value="' + compareData.replace(/"/g, '&quot;') + '">'
374            );
375
376            var listToFetch = JSON.parse(compareData);
377            if (listToFetch.length > 0) {
378                $.ajax({
379                    url: '/wp-admin/admin-ajax.php',
380                    type: 'POST',
381                    data: {
382                        action: 'p2s_get_quote_installers_info',
383                        installer_ids: JSON.stringify(listToFetch)
384                    },
385                    success: function(res) {
386                        if (res.success && res.data && res.data.length > 0) {
387                            var items = res.data;
388                            var count = items.length;
389
390                            var updatedList = items.map(function(item) {
391                                return { id: String(item.id), name: item.title };
392                            });
393                            localStorage.setItem('p2s_compare_list', JSON.stringify(updatedList));
394
395                            if (count === 1) {
396                                var name = items[0].title;
397                                $('.p2s-quote-heading .elementor-heading-title, .p2s-quote-heading h1').text('Request a quote');
398                                $('.p2s-quote-subtitle p, .p2s-quote-subtitle').text("Send your details to " + name + ". They'll get back to you shortly.");
399                                $('.p2s-custom-quote-form input[type="submit"]').val("Request a quote from " + name);
400                                $('.p2s-quote-tip-box .elementor-widget-container, .p2s-quote-tip-box').html(
401                                    '<b>💡 One enquiry, multiple quotes.</b> Go back and click \'Add to quote\' on more installers to request from all of them at once.'
402                                );
403                            } else {
404                                $('.p2s-quote-heading .elementor-heading-title, .p2s-quote-heading h1').text('Request quotes from ' + count + ' installers');
405                                $('.p2s-quote-subtitle p, .p2s-quote-subtitle').text('One form, ' + count + ' installers. Let the quotes come to you — no chasing required.');
406                                $('.p2s-custom-quote-form input[type="submit"]').val('Request quotes from ' + count + ' installers');
407                                $('.p2s-quote-tip-box .elementor-widget-container, .p2s-quote-tip-box').html(
408                                    '<b>💡 One enquiry, multiple quotes.</b> Getting quotes from multiple installers side by side typically saves the most — you’re all set.'
409                                );
410                            }
411
412                            var listHtml = count > 1 ? '<div class="p2s-installers-list">' : '';
413                            items.forEach(function(item) {
414                                if (count === 1) {
415                                    var logoHtml = item.logo ? 
416                                        '<div class="p2s-installer-logo-wrap"><img class="p2s-installer-logo" src="' + item.logo + '" alt="' + item.title + '"></div>' : 
417                                        '<div class="p2s-installer-avatar"><span class="sc-interp">' + item.initials + '</span></div>';
418                                    
419                                    var rating = item.meta.split(' ')[0] || '4.9';
420                                    var reviews = item.meta.substring(item.meta.indexOf(' ')) || ' (214)';
421
422                                    listHtml += '<div class="p2s-installer-item p2s-installer-single">' +
423                                        logoHtml +
424                                        '<div class="p2s-installer-details">' +
425                                            '<div class="p2s-installer-title"><span class="sc-interp">' + item.title + '</span></div>' +
426                                        '</div>' +
427                                    '</div>';
428                                } else {
429                                    var logoHtml = item.logo ? 
430                                        '<div class="p2s-installer-logo-wrap"><img class="p2s-installer-logo" src="' + item.logo + '" alt="' + item.title + '"></div>' : 
431                                        '<div class="p2s-installer-avatar"><span class="sc-interp">' + item.initials + '</span></div>';
432                                    
433                                    var ratingParts = item.meta.split(' &middot; ');
434                                    var rating = ratingParts[0] || '4.9';
435                                    var distance = ratingParts[1] || '';
436                                    var metaHtml = distance ? '<span class="p2s-installer-distance"><span class="sc-interp">' + distance + '</span></span>' : '';
437
438                                    listHtml += '<div class="p2s-installer-item p2s-installer-multiple">' +
439                                        logoHtml +
440                                        '<div class="p2s-installer-details">' +
441                                            '<div class="p2s-installer-title"><span class="sc-interp">' + item.title + '</span></div>' +
442                                            '<div class="p2s-installer-meta">' + metaHtml + '</div>' +
443                                        '</div>' +
444                                    '</div>';
445                                }
446                            });
447                            if (count > 1) {
448                                listHtml += '</div>';
449                            }
450                            $('.p2s-contacting-list .elementor-widget-container, .p2s-contacting-list').html(listHtml);
451                        }
452                    }
453                });
454            }
455        }
456
457        $(document).on('click', '.p2s-quote-back a', function(e) {
458            e.preventDefault();
459            if (document.referrer && document.referrer.indexOf(window.location.host) !== -1) {
460                window.history.back();
461            } else {
462                window.location.href = '/find-an-installer/';
463            }
464        });
465
466        var refCode = urlParams.get('ref');
467
468        if (urlParams.has('updated') || refCode) {
469            localStorage.removeItem('p2s_compare_list');
470            if (refCode) {
471                $('.p2s-ref-code strong').text(refCode);
472            }
473            $('.before-installer-quote-form').hide();
474            $('.after-installer-quote-form').show();
475
476            if (window.history.replaceState) {
477                window.history.replaceState({}, document.title, window.location.pathname);
478            }
479        } else {
480            $('.after-installer-quote-form').hide();
481        }
482
483        function renderCompareTray() {
484            var list = JSON.parse(localStorage.getItem('p2s_compare_list') || '[]');
485            var count = list.length;
486
487            if (count > 0) {
488                var html = '';
489                list.forEach(function(item) {
490                    html += '<div class="p2s-tray-item">' + 
491                            item.name + 
492                            '<button type="button" class="p2s-tray-remove-btn" data-name="' + item.name + '">×</button>' + 
493                            '</div>';
494                });
495
496                $('.p2s-tray-items').html(html);
497                $('.p2s-tray-count').html('<span>' + count + '</span>/3 selected');
498                if (count === 1) {
499                    $('.p2s-tray-quote-btn').html('Request quote from <span class="sc-interp">1</span> installer →');
500                } else {
501                    $('.p2s-tray-quote-btn').html('Request quotes from <span class="sc-interp">' + count + '</span> installers →');
502                }
503                $('.p2s-compare-tray').show();
504            } else {
505                $('.p2s-compare-tray').hide();
506                $('.p2s-tray-items').empty();
507            }
508
509            var ids = list.map(function(item) { return String(item.id); });
510            var isMaxReached = list.length >= 3;
511            $('.p2s-installer-card').each(function() {
512                var btn = $(this).find('.p2s-btn-quote');
513                var btnId = String(btn.attr('data-id'));
514                if (ids.indexOf(btnId) !== -1) {
515                    btn.addClass('p2s-btn-added-state').removeClass('p2s-btn-disabled').text('✓ Added');
516                    btn.removeAttr('title');
517                } else if (list.length > 0) {
518                    btn.removeClass('p2s-btn-added-state').text('Add to quote');
519                    if (isMaxReached) {
520                        btn.addClass('p2s-btn-disabled');
521                        btn.attr('title', 'Maximum of 3 quotes allowed');
522                    } else {
523                        btn.removeClass('p2s-btn-disabled');
524                        btn.removeAttr('title');
525                    }
526                } else {
527                    btn.removeClass('p2s-btn-added-state p2s-btn-disabled').text('Get a quote');
528                    btn.removeAttr('title');
529                }
530            });
531        }
532
533        renderCompareTray();
534
535        $('input[name="installer-postcode"]').on('keypress', function(e) {
536            if (e.which === 13) {
537                e.preventDefault();
538                $('.installer-step-one-btn').click();
539            }
540        });
541
542        function getSavedInstallers() {
543            try {
544                return JSON.parse(localStorage.getItem('p2s_saved_installers')) || [];
545            } catch (e) {
546                return [];
547            }
548        }
549
550        function setSavedInstallers(list) {
551            localStorage.setItem('p2s_saved_installers', JSON.stringify(list));
552            updateSavedCount();
553        }
554
555        function updateSavedCount() {
556            var count = getSavedInstallers().length;
557            var $btn = $('.p2s-saved-btn');
558            if ($btn.length) {
559                $btn.find('.sc-interp').text(count);
560                if ($btn.hasClass('p2s-chip-active') && count === 0) {
561                    // Optional: If they are viewing the "Saved" filter and remove the last one, it stays on 0.
562                }
563            }
564        }
565        updateSavedCount();
566
567        function initProfileSaveButton() {
568            var $btn = $('.p2s-profile-save-btn');
569            if (!$btn.length) return;
570
571            var id = String($btn.attr('data-id'));
572            var saved = getSavedInstallers();
573
574            if (saved.indexOf(id) !== -1) {
575                $btn.addClass('active').html('♥ Saved');
576            } else {
577                $btn.removeClass('active').html('♡ Save');
578            }
579        }
580
581        initProfileSaveButton();
582
583        $(document).on('click', '.p2s-profile-save-btn', function() {
584            var $btn = $(this);
585            var id = String($btn.attr('data-id'));
586            if (!id) return;
587
588            var saved = getSavedInstallers();
589            var index = saved.indexOf(id);
590
591            if (index !== -1) {
592                saved.splice(index, 1);
593                $btn.removeClass('active').html('♡ Save');
594            } else {
595                saved.push(id);
596                $btn.addClass('active').html('♥ Saved');
597            }
598
599            setSavedInstallers(saved);
600        });
601
602        $(document).on('click', '.p2s-card-saved-btn', function() {
603            var $btn = $(this);
604            var id = String($btn.attr('data-id'));
605            if (!id) return;
606
607            var saved = getSavedInstallers();
608            var index = saved.indexOf(id);
609
610            if (index !== -1) {
611                saved.splice(index, 1);
612                setSavedInstallers(saved);
613                
614                if ($('.p2s-saved-btn').hasClass('p2s-chip-active')) {
615                    var $grid = $btn.closest('.p2s-installers-grid');
616                    $btn.closest('.p2s-installer-card').remove();
617                    
618                    var currentCount = parseInt($('.p2s-results-count span').text()) || 0;
619                    if (currentCount > 0) {
620                        currentCount--;
621                        $('.p2s-results-count span').text(currentCount);
622                        $('.p2s-location-sub').show().find('.sc-interp').text(currentCount);
623                    }
624
625                    if ($grid.find('.p2s-installer-card').length === 0) {
626                        $('.p2s-empty-state').show();
627                    }
628                } else {
629                    $btn.closest('.p2s-card-saved-wrap').remove();
630                }
631            }
632        });
633
634        /* Photo Lightbox Functionality */
635        const $galleryItems = $('.p2s-gallery-item img');
636        const $lightbox = $('#p2s-lightbox');
637        
638        if ($galleryItems.length && $lightbox.length) {
639            const $lightboxImg = $('#p2s-lightbox-img');
640            const $counter = $('#p2s-lightbox-counter');
641            
642            const images = [];
643            $galleryItems.each(function() {
644                images.push($(this).attr('src').replace('&w=600&', '&w=1400&'));
645            });
646            let currentIndex = 0;
647
648            function updateLightbox() {
649                $lightboxImg.attr('src', images[currentIndex]);
650                $counter.text((currentIndex + 1) + ' / ' + images.length);
651            }
652
653            function closeLightbox() {
654                $lightbox.removeClass('active').attr('aria-hidden', 'true');
655                $('body').css('overflow', '');
656            }
657
658            function showPrev() {
659                currentIndex = (currentIndex - 1 + images.length) % images.length;
660                updateLightbox();
661            }
662
663            function showNext() {
664                currentIndex = (currentIndex + 1) % images.length;
665                updateLightbox();
666            }
667
668            $galleryItems.on('click', function() {
669                currentIndex = $galleryItems.index(this);
670                updateLightbox();
671                $lightbox.addClass('active').attr('aria-hidden', 'false');
672                $('body').css('overflow', 'hidden');
673            });
674
675            $('#p2s-lightbox-close').on('click', closeLightbox);
676            $('#p2s-lightbox-prev').on('click', function(e) { e.stopPropagation(); showPrev(); });
677            $('#p2s-lightbox-next').on('click', function(e) { e.stopPropagation(); showNext(); });
678
679            $lightbox.on('click', function(e) {
680                if ($(e.target).is($lightbox) || $(e.target).hasClass('p2s-lightbox-content')) {
681                    closeLightbox();
682                }
683            });
684
685            $(document).on('keydown', function(e) {
686                if (!$lightbox.hasClass('active')) return;
687                if (e.key === 'Escape') closeLightbox();
688                if (e.key === 'ArrowLeft') showPrev();
689                if (e.key === 'ArrowRight') showNext();
690            });
691        }
692    });
693})(jQuery);

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.