PageSourceSearch

https://alleyadoma.ru/designers_tips/app.js?v=1786637584

js alleyadoma.ru collected 2026-10-07 17:59:21 UTC 78,386 bytes, 1,810 lines download raw bytes

1// app.js
2
3// Защита от использования модуля на сторонних доменах (Domain Lock)
4(function() {
5    const allowedDomains = ['alleyadoma.ru', 'localhost', '127.0.0.1'];
6    const currentHost = window.location.hostname;
7    const isLocalFile = window.location.protocol === 'file:';
8    
9    const isDomainAllowed = isLocalFile || allowedDomains.some(domain => {
10        return currentHost === domain || currentHost.endsWith('.' + domain);
11    });
12    
13    if (!isDomainAllowed) {
14        console.warn('[Security] Использование данного модуля на этом домене запрещено.');
15        document.addEventListener('DOMContentLoaded', (e) => {
16            e.stopImmediatePropagation();
17            const slider = document.querySelector('.designer-advice-slider');
18            if (slider) {
19                slider.innerHTML = '<div style="padding: 25px; text-align: center; color: #CF003A; font-weight: 600; background: #FFF; border-radius: 16px; border: 1px solid #DFE0E5;">Ошибка безопасности: Использование модуля на стороннем домене запрещено.</div>';
20            }
21        }, true);
22    }
23})();
24
25// Хелпер экранирования HTML для предотвращения XSS
26function escapeHTML(str) {
27    if (!str) return '';
28    return str.toString()
29        .replace(/&/g, '&amp;')
30        .replace(/</g, '&lt;')
31        .replace(/>/g, '&gt;')
32        .replace(/"/g, '&quot;')
33        .replace(/'/g, '&#39;');
34}
35
36// Хелпер безопасного добавления слушателя событий (для элементов конструктора на боевом сайте)
37function safeAddListener(element, event, callback) {
38    if (element) {
39        element.addEventListener(event, callback);
40    }
41}
42
43// Хелпер отправки событий аналитики (Яндекс.Метрика и Google Analytics)
44function sendAnalyticsEvent(eventName, params = {}) {
45    console.log(`[Analytics] Отправка события: ${eventName}`, params);
46    
47    // 1. Google Analytics (GA4 / gtag.js)
48    if (typeof gtag === 'function') {
49        gtag('event', eventName, params);
50    }
51    
52    // 2. Яндекс.Метрика
53    if (typeof ym === 'function') {
54        try {
55            const counters = window.Ya && window.Ya.Metrika && window.Ya.Metrika.counters;
56            if (counters && counters.length > 0) {
57                counters.forEach(counter => {
58                    ym(counter.id, 'reachGoal', eventName, params);
59                });
60            } else {
61                const metrikaCounters = window.Ya && window.Ya._metrika && window.Ya._metrika.counters;
62                if (metrikaCounters) {
63                    Object.keys(metrikaCounters).forEach(id => {
64                        ym(id, 'reachGoal', eventName, params);
65                    });
66                } else if (window.yandexMetrikaId) {
67                    ym(window.yandexMetrikaId, 'reachGoal', eventName, params);
68                }
69            }
70        } catch (e) {
71            console.warn('[Analytics] Ошибка при отправке в Яндекс.Метрику:', e);
72        }
73    }
74}
75
76// 1. Инициализация состояния приложения
77const STORAGE_KEY = 'designer_carousel_slides';
78let slides = [];
79function initSlidesData() {
80    if (slides && slides.length > 0) return;
81    
82    // Пытаемся взять из localStorage
83    try {
84        const savedSlides = localStorage.getItem(STORAGE_KEY);
85        if (savedSlides) {
86            slides = JSON.parse(savedSlides);
87            return;
88        }
89    } catch (e) {
90        console.error("Ошибка загрузки данных из LocalStorage:", e);
91    }
92    
93    // Если нет в localStorage, берем из data.js
94    if (typeof initialSlidesData !== 'undefined' && Array.isArray(initialSlidesData)) {
95        slides = [...initialSlidesData];
96    } else {
97        console.warn("[Designer tips] Переменная initialSlidesData не найдена или не явÐ
97»ÑÐµÑ‚ся массивом. Будет использован пустой слайдер.");
98        slides = [];
99    }
100}
101
102let currentSlideIndex = 0;
103let selectedMarkerId = null;
104let activeExportType = 'json'; // 'json' или 'html'
105let isDragging = false;
106let dragMarkerId = null;
107
108// История действий (Undo / Redo)
109let undoStack = [];
110let redoStack = [];
111const MAX_HISTORY_LIMIT = 30;
112let stateBeforeInput = null;
113let stateBeforeDrag = null;
114
115// Автоматическое перелистывание (Autoplay)
116let autoplayInterval = null;
117let isAutoplayPaused = false;
118let isMouseInsideSlider = false;
119let autoplayProgress = 0;
120const AUTOPLAY_DURATION = 7000;
121const PROGRESS_INTERVAL = 30; // интервал обновления в мс
122
123// Функция для сохранения данных
124function saveToLocalStorage() {
125    try {
126        localStorage.setItem(STORAGE_KEY, JSON.stringify(slides));
127    } catch (e) {
128        console.error("Ошибка сохранения данных в LocalStorage:", e);
129    }
130}
131
132// Управление историей изменений (Undo / Redo)
133function pushHistory() {
134    const stateStr = JSON.stringify(slides);
135    if (undoStack.length > 0 && undoStack[undoStack.length - 1] === stateStr) {
136        return;
137    }
138    undoStack.push(stateStr);
139    if (undoStack.length > MAX_HISTORY_LIMIT) {
140        undoStack.shift();
141    }
142    redoStack = [];
143    updateHistoryButtons();
144}
145
146function undo() {
147    if (undoStack.length === 0) return;
148    redoStack.push(JSON.stringify(slides));
149    const prevState = undoStack.pop();
150    slides = JSON.parse(prevState);
151    initBuilder();
152    renderSlider();
153    updateHistoryButtons();
154}
155
156function redo() {
157    if (redoStack.length === 0) return;
158    undoStack.push(JSON.stringify(slides));
159    const nextState = redoStack.pop();
160    slides = JSON.parse(nextState);
161    initBuilder();
162    renderSlider();
163    updateHistoryButtons();
164}
165
166function updateHistoryButtons() {
167    const undoBtn = document.getElementById('undo-btn');
168    const redoBtn = document.getElementById('redo-btn');
169    if (undoBtn) undoBtn.disabled = undoStack.length === 0;
170    if (redoBtn) redoBtn.disabled = redoStack.length === 0;
171}
172
173// Управление автопрокруткой (7 секунд)
174function startAutoplay(resetProgress = true) {
175    stopAutoplay();
176    
177    // Не запускаем автопрокрутку на вкладке конструктора
178    const previewTab = document.getElementById('preview-tab');
179    if (!previewTab || !previewTab.classList.contains('active')) {
180        return;
181    }
182    
183    isAutoplayPaused = isMouseInsideSlider;
184    if (isAutoplayPaused) {
185        if (resetProgress) {
186            autoplayProgress = 0;
187            updateProgressBar(0);
188        }
189        return;
190    }
191    
192    if (resetProgress) {
193        autoplayProgress = 0;
194    }
195    
196    updateProgressBar(autoplayProgress / AUTOPLAY_DURATION);
197    
198    autoplayInterval = setInterval(() => {
199        const hasActiveTooltip = document.querySelector('.complect-marker.active') !== null;
200        if (hasActiveTooltip) {
201            // Замораживаем прогресс, если открыто всплывающее окно
202            return;
203        }
204        
205        autoplayProgress += PROGRESS_INTERVAL;
206        updateProgressBar(autoplayProgress / AUTOPLAY_DURATION);
207        
208        if (autoplayProgress >= AUTOPLAY_DURATION) {
209            autoplayProgress = 0;
210            goToSlide(currentSlideIndex + 1);
211        }
212    }, PROGRESS_INTERVAL);
213}
214
215function stopAutoplay() {
216    if (autoplayInterval) {
217        clearInterval(autoplayInterval);
218        autoplayInterval = null;
219    }
220}
221
222function pauseAutoplay() {
223    isAutoplayPaused = true;
224    stopAutoplay();
225}
226
227function resumeAutoplay() {
228    isAutoplayPaused = false;
229    startAutoplay(false); // Продолжаем с того же места
230}
231
232function updateProgressBar(ratio) {
233    const progressBar = document.getElementById('autoplay-progress-bar');
234    if (progressBar) {
235        const pct = Math.max(0, Math.min(100, ratio * 100));
236        progressBar.style.width = `${pct}%`;
237        
238        // Плавный переход цвета: от светло-салатового (75) к насыщенному фирменному зеленому (95)
239        const hue = 75 + (95 - 75) * ratio;
240        const lightness = 55 - (55 - 37) * ratio;
241        const saturation = 50 + (65 - 50) * ratio;
242        
243        progressBar.style.backgroundColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
244        
245        // Нарастающее неоновое свечение
246        progressBar.style.boxShadow = `0 1px 8px rgba(85, 151, 35, ${0.1 + ratio * 0.45})`;
247    }
248}
249
250// Элементы DOM для карусели
251let sliderWrapper, sliderPrev, sliderNext, sliderPagination;
252let designerImg, designerName, slideCollectionName, slideCollectionDesc, slideCollectionLink, slideBadge, designerTitleLabel;
253
254// Элементы DOM для вкладок
255let tabButtons, tabContents;
256
257// Элементы DOM для конструктора
258let builderCanvas, builderImagePreview, builderMarkersContainer, slideSelect, addSlideBtn, deleteSlideBtn;
259
260// Поля ввода слайда
261let inputSlideName, inputSlideDesc, inputSlideImage, inputSlideLink, inputSlideBadge, inputSlideDesignerTitle, inputDesignerName, inputDesignerAvatar;
262
263// Редактор маркера
264let markerEditorSection, deleteMarkerBtn, markerCoordX, markerCoordY;
265let inputMarkerTitle, inputMarkerPrice, inputMarkerDiscount, calculatedPriceBadge, inputMarkerLink, fetchProductBtn, importStatusMsg;
266
267// Экспорт кода
268let exportTabButtons, exportTextarea, copyCodeBtn;
269
270// Функция для безопасного поиска элементов в DOM (вызывается при DOMContentLoaded)
271function initDOMElements() {
272    sliderWrapper = document.getElementById('slider-wrapper');
273    sliderPrev = document.getElementById('slider-prev');
274    sliderNext = document.getElementById('slider-next');
275    sliderPagination = document.getElementById('slider-pagination');
276    designerImg = document.getElementById('designer-img');
277    designerName = document.getElementById('designer-name');
278    slideCollectionName = document.getElementById('slide-collection-name');
279    slideCollectionDesc = document.getElementById('slide-collection-desc');
280    slideCollectionLink = document.getElementById('slide-collection-link');
281    slideBadge = document.getElementById('slide-badge');
282    designerTitleLabel = document.getElementById('designer-title');
283
284    tabButtons = document.querySelectorAll('.tab-btn');
285    tabContents = document.querySelectorAll('.tab-content');
286
287    builderCanvas = document.getElementById('builder-canvas');
288    builderImagePreview = document.getElementById('builder-image-preview');
289    builderMarkersContainer = document.getElementById('builder-markers-container');
290    slideSelect = document.getElementById('slide-select');
291    addSlideBtn = document.getElementById('add-new-slide-btn');
292    deleteSlideBtn = document.getElementById('delete-slide-btn');
293
294    inputSlideName = document.getElementById('input-slide-name');
295    inputSlideDesc = document.getElementById('input-slide-desc');
296    inputSlideImage = document.getElementById('input-slide-image');
297    inputSlideLink = document.getElementById('input-slide-link');
298    inputSlideBadge = document.getElementById('input-slide-badge');
299    inputSlideDesignerTitle = document.getElementById('input-slide-designer-title');
300    inputDesignerName = document.getElementById('input-designer-name');
301    inputDesignerAvatar = document.getElementById('input-designer-avatar');
302
303    markerEditorSection = document.getElementById('marker-editor-section');
304    deleteMarkerBtn = document.getElementById('delete-marker-btn');
305    markerCoordX = document.getElementById('marker-coord-x');
306    markerCoordY = document.getElementById('marker-coord-y');
307    inputMarkerTitle = document.getElementById('input-marker-title');
308    inputMarkerPrice = document.getElementById('input-marker-price');
309    inputMarkerDiscount = document.getElementById('input-marker-discount');
310    calculatedPriceBadge = document.getElementById('calculated-price-badge');
311    inputMarkerLink = document.getElementById('input-marker-link');
312    fetchProductBtn = document.getElementById('fetch-product-btn');
313    importStatusMsg = document.getElementById('import-status-msg');
314
315    exportTabButtons = document.querySelectorAll('.export-tab-btn');
316    exportTextarea = document.getElementById('export-textarea');
317    copyCodeBtn = document.getElementById('copy-code-btn');
318}
319
320// ==========================================================================
321// 2. Логика интерактивного слайдера (Live Preview)
322// ==========================================================================
323
324// Форматирование чисел (добавление пробелов как разделителей тысяч)
325function formatPrice(value) {
326    if (!value) return '0';
327    // Очистить от пробелов и нецифровых символов
328    const cleanValue = value.toString().replace(/\s/g, '').replace(/\D/g, '');
329    if (!cleanValue) return '0';
330    return Number(cleanValue).toLocaleString('ru-RU');
331}
332
333// Определение оптимального направления тултипа относительно координат маркера
334function getTooltipPositionClass(x, y) {
335    // Сначала проверяем горизонтальные границы для боковых точек
336    if (x < 25) return 'pos-right'; // Слишком близко к левому краю -> открываем вправо
337    if (x > 75) return 'pos-left';  // Слишком близко к правому краю -> открываем влево
338    // Порог вертикали: если точка в верхней половине (y < 45%) -> раскрываем вниз
339    if (y < 45) return 'pos-bottom';
340    // В нижней половине -> раскрываем вверх
341    return 'pos-top';
342}
343
344// Рендеринг слайдов и маркеров в предпросмотре
345function renderSlider() {
346    sliderWrapper.innerHTML = '<div class="autoplay-progress-bar" id="autoplay-progress-bar"></div>';
347    sliderPagination.innerHTML = '';
348    
349    if (slides.length === 0) {
350        sliderWrapper.innerHTML = '<div class="slider-empty-state">Нет доступных слайдов. Создайте слайд в конструкторе!</div>';
351        return;
352    }
353    
354    // Рендерим слайды
355    slides.forEach((slide, sIdx) => {
356        const slideItem = document.createElement('div');
357        slideItem.className = `slider-item ${sIdx === currentSlideIndex ? 'active' : ''}`;
358        
359        const img = document.createElement('img');
360        const isCurrent = sIdx === currentSlideIndex;
361        const realSrc = slide.image || 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1200&q=80';
362        
363        if (isCurrent) {
364            img.src = realSrc;
365            img.setAttribute('fetchpriority', 'high');
366        } else {
367            img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
368            img.dataset.src = realSrc;
369            img.loading = 'lazy';
370        }
371        
372        img.alt = slide.name;
373        slideItem.appendChild(img);
374        
375        // Создаем контейнер точек
376        const markersContainer = document.createElement('div');
377        markersContainer.className = 'complect-markers';
378        
379        slide.markers.forEach((marker) => {
380            const markerEl = document.createElement('div');
381            markerEl.className = 'complect-marker';
382            markerEl.style.setProperty('--x', marker.x);
383            markerEl.style.setProperty('--y', marker.y);
384            
385            const btn = document.createElement('span');
386            btn.className = 'complect-marker__btn';
387            btn.setAttribute('data-href', marker.link || '#');
388            markerEl.appendChild(btn);
389            
390            // Тултип
391            const tooltip = document.createElement('div');
392            const posClass = getTooltipPositionClass(marker.x, marker.y);
393            tooltip.className = `complect-marker__tooltip ${posClass}`;
394            
395            // Расчет цен
396            const discountPercent = parseInt(marker.discount) || 0;
397            const originalPriceFormatted = formatPrice(marker.price);
398            const currentPriceFormatted = formatPrice(marker.priceWithDiscount || marker.price);
399            
400            let priceRowHTML = '';
401            if (discountPercent > 0) {
402                priceRowHTML = `
403                    <span class="tooltip-price-current">${currentPriceFormatted} ₽</span>
404                    <span class="tooltip-price-old">${originalPriceFormatted} ₽</span>
405                    <span class="tooltip-discount-badge">-${discountPercent}%</span>
406                `;
407            } else {
408                priceRowHTML = `<span class="tooltip-price-current">${currentPriceFormatted} ₽</span>`;
409            }
410            
411            tooltip.innerHTML = `
412                <div class="tooltip-header">
413                    <div class="tooltip-title">${escapeHTML(marker.title)}</div>
414                    <button class="tooltip-close" aria-label="Закрыть"><i class="fa-solid fa-xmark"></i></button>
415                </div>
416                <div class="tooltip-price-row">
417                    ${priceRowHTML}
418                </div>
419                <div class="tooltip-footer">
420                    <a href="${escapeHTML(marker.link || '#')}" target="_blank" class="tooltip-action-btn" title="Перейти Ð
420º товару">
421                        <i class="fa-solid fa-chevron-right"></i>
422                    </a>
423                </div>
424            `;
425            
426            // Отслеживание перехода в карточку товара
427            tooltip.querySelector('.tooltip-action-btn').addEventListener('click', () => {
428                sendAnalyticsEvent('Advice_Product_Go', {
429                    slide_index: sIdx + 1,
430                    slide_name: slide.name,
431                    product_title: marker.title,
432                    product_link: marker.link || '#'
433                });
434            });
435            
436            // Обработчик для кликов по крестику на мобильных устройствах
437            tooltip.querySelector('.tooltip-close').addEventListener('click', (e) => {
438                e.stopPropagation();
439                markerEl.classList.remove('active');
440            });
441            
442            // Интерактив для мобильных (клик по точке переключает тултип)
443            markerEl.addEventListener('click', (e) => {
444                e.stopPropagation();
445                
446                if (e.target.closest('.tooltip-action-btn') || e.target.closest('.tooltip-close')) {
447                    return;
448                }
449                
450                // Закрываем другие активные тултипы на слайде
451                const activeMarkers = slideItem.querySelectorAll('.complect-marker.active');
452                activeMarkers.forEach(am => {
453                    if (am !== markerEl) am.classList.remove('active');
454                });
455                
456                const isOpening = !markerEl.classList.contains('active');
457                markerEl.classList.toggle('active');
458                
459                if (isOpening) {
460                    sendAnalyticsEvent('Advice_Hotspot_Click', {
461                        slide_index: sIdx + 1,
462                        slide_name: slide.name,
463                        product_title: marker.title,
464                        product_price: marker.priceWithDiscount || marker.price
465                    });
466                }
467            });
468            
469            markerEl.appendChild(tooltip);
470            markersContainer.appendChild(markerEl);
471        });
472        
473        slideItem.appendChild(markersContainer);
474        sliderWrapper.appendChild(slideItem);
475        
476        // Создаем точку пагинации
477        const bullet = document.createElement('span');
478        bullet.className = `pagination-bullet ${sIdx === currentSlideIndex ? 'active' : ''}`;
479        bullet.addEventListener('click', () => {
480            goToSlide(sIdx, true);
481        });
482        sliderPagination.appendChild(bullet);
483    });
484    
485    // Обновляем информацию дизайнера справа
486    const activeSlide = slides[currentSlideIndex];
487    if (activeSlide) {
488        designerImg.src = activeSlide.designer?.avatar || 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=150&q=80';
489        designerImg.alt = activeSlide.designer?.name || 'Дизайнер';
490        designerName.textContent = activeSlide.designer?.name || 'Виктор';
491        slideCollectionName.textContent = activeSlide.name;
492        slideCollectionDesc.textContent = activeSlide.description;
493        if (slideCollectionLink) slideCollectionLink.href = activeSlide.link || '#';
494        slideBadge.textContent = activeSlide.badge || 'СОВЕТЫ ДИЗАЙНЕРА';
495        designerTitleLabel.textContent = activeSlide.designerTitle || 'Искусство интерьера';
496    }
497}
498
499// Клик вне маркеров закрывает все активные тултипы
500document.addEventListener('click', () => {
501    const activeMarkers = document.querySelectorAll('.complect-marker.active');
502    activeMarkers.forEach(m => m.classList.remove('active'));
503});
504
505// Навигация
506function goToSlide(index, isManual = false) {
507    // Сбрасываем и перезапускаем таймер автопрокрутки
508    const previewTab = document.getElementById('preview-tab');
509    if (previewTab && previewTab.classList.contains('active')) {
510        startAutoplay();
511    }
512
513    if (index < 0) {
514        currentSlideIndex = slides.length - 1;
515    } else if (index >= slides.length) {
516        currentSlideIndex = 0;
517    } else {
518        currentSlideIndex = index;
519    }
520    
521    // Обновляем классы активности
522    const items = sliderWrapper.querySelectorAll('.slider-item');
523    items.forEach((item, idx) => {
524        item.classList.toggle('active', idx === currentSlideIndex);
525    });
526    
527    // Lazy Loading: Загружаем картинку текущего активного слайда
528    const activeItem = items[currentSlideIndex];
529    if (activeItem) {
530        const activeImg = activeItem.querySelector('img');
531        if (activeImg && activeImg.dataset.src && activeImg.src !== activeImg.dataset.src) {
532            activeImg.src = activeImg.dataset.src;
533        }
534    }
535    
536    // Pre-fetching: Упреждающе загружаем картинку следующего слайда для бесшовного перехода
537    if (items.length > 1) {
538        const nextIndex = (currentSlideIndex + 1) % items.length;
539        const nextItem = items[nextIndex];
540        if (nextItem) {
541            const nextImg = nextItem.querySelector('img');
542            if (nextImg && nextImg.dataset.src && nextImg.src !== nextImg.dataset.src) {
543                nextImg.src = nextImg.dataset.src;
544            }
545        }
546    }
547    
548    const bullets = sliderPagination.querySelectorAll('.pagination-bullet');
549    bullets.forEach((bullet, idx) => {
550        bullet.classList.toggle('active', idx === currentSlideIndex);
551    });
552    
553    // Обновляем данные дизайнера
554    const activeSlide = slides[currentSlideIndex];
555    if (activeSlide) {
556        designerImg.src = activeSlide.designer?.avatar;
557        designerName.textContent = activeSlide.designer?.name;
558        slideCollectionName.textContent = activeSlide.name;
559        slideCollectionDesc.textContent = activeSlide.description;
560        if (slideCollectionLink) slideCollectionLink.href = activeSlide.link || '#';
561        slideBadge.textContent = activeSlide.badge || 'СОВЕТЫ ДИЗАЙНЕРА';
562        designerTitleLabel.textContent = activeSlide.designerTitle || 'Искусство интерьера';
563        
564        // Отправляем событие аналитики при ручном перелистывании
565        if (isManual) {
566            sendAnalyticsEvent('Advice_Slide_Change', {
567                slide_index: currentSlideIndex + 1,
568                slide_name: activeSlide.name
569            });
570        }
571    }
572    
573    // Если мы в конструкторе, нужно переинициализировать его
574    const builderTab = document.getElementById('builder-tab');
575    if (builderTab && builderTab.classList.contains('active')) {
576        initBuilder();
577    }
578}
579
580// Привязка основных событий слайдера и табов (вызывается после DOMContentLoaded)
581function initMainEvents() {
582    if (sliderPrev) sliderPrev.addEventListener('click', () => goToSlide(currentSlideIndex - 1, true));
583    if (sliderNext) sliderNext.addEventListener('click', () => goToSlide(currentSlideIndex + 1, true));
584    
585    if (tabButtons && tabButtons.length > 0) {
586        tabButtons.forEach(btn => {
587            btn.addEventListener('click', () => {
588                tabButtons.forEach(b => b.classList.remove('active'));
589                tabContents.forEach(c => c.classList.remove('active'));
590                
591                btn.classList.add('active');
592                const targetTab = btn.getAttribute('data-tab');
593                const targetTabEl = document.getElementById(`${targetTab}-tab`);
594                if (targetTabEl) targetTabEl.classList.add('active');
595                
596                if (targetTab === 'builder') {
597                    stopAutoplay();
598                    initBuilder();
599                } else {
600                    renderSlider();
601                    startAutoplay();
602                }
603            });
604        });
605    }
606}
607
608// ==========================================================================
609// 4. Логика Конструктора (Visual Builder)
610// ==========================================================================
611
612function initBuilder() {
613    // 4.1 Заполняем выпадающий список слайдов
614    slideSelect.innerHTML = '';
615    slides.forEach((slide, idx) => {
616        const option = document.createElement('option');
617        option.value = idx;
618        option.textContent = `${idx + 1}. ${slide.name || 'Слайд без названия'}`;
619        option.selected = idx === currentSlideIndex;
620        slideSelect.appendChild(option);
621    });
622    
623    const currentSlide = slides[currentSlideIndex];
624    if (!currentSlide) {
625        builderImagePreview.src = '';
626        builderMarkersContainer.innerHTML = '';
627        markerEditorSection.style.display = 'none';
628        return;
629    }
630    
631    // 4.2 Синхронизируем форму редактирования слайда
632    inputSlideName.value = currentSlide.name || '';
633    inputSlideDesc.value = currentSlide.description || '';
634    inputSlideImage.value = currentSlide.image || '';
635    if (inputSlideLink) inputSlideLink.value = currentSlide.link || '';
636    inputSlideBadge.value = currentSlide.badge || 'СОВЕТЫ ДИЗАЙНЕРА';
637    inputSlideDesignerTitle.value = currentSlide.designerTitle || 'Искусство интерьера';
638    inputDesignerName.value = currentSlide.designer?.name || 'Виктор';
639    inputDesignerAvatar.value = currentSlide.designer?.avatar || '';
640    
641    // 4.3 Устанавливаем фон холста
642    builderImagePreview.src = currentSlide.image || 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1200&q=80';
643    
644    // Обновляем превью в зоне загрузки аватара
645    const avatarPreviewImg = document.getElementById('avatar-drop-zone-preview');
646    const avatarIcon = document.getElementById('avatar-drop-zone-icon');
647    if (avatarPreviewImg && avatarIcon) {
648        if (currentSlide.designer?.avatar) {
649            avatarPreviewImg.src = currentSlide.designer.avatar;
650            avatarPreviewImg.style.display = 'block';
651            avatarIcon.style.display = 'none';
652        } else {
653            avatarPreviewImg.src = '';
654            avatarPreviewImg.style.display = 'none';
655            avatarIcon.style.display = 'block';
656        }
657    }
658    
659    // 4.4 Отрисовываем маркеры на холсте конструктора
660    renderBuilderMarkers();
661    
662    // Скрываем редактор товара пока ничего не выбрано
663    if (!selectedMarkerId || !currentSlide.markers.find(m => m.id === selectedMarkerId)) {
664        selectedMarkerId = null;
665        markerEditorSection.style.display = 'none';
666    } else {
667        selectMarker(selectedMarkerId);
668    }
669    
670    updateExportCode();
671}
672
673// Отрисовка маркеров на холсте редактора
674function renderBuilderMarkers() {
675    builderMarkersContainer.innerHTML = '';
676    const currentSlide = slides[currentSlideIndex];
677    if (!currentSlide) return;
678    
679    currentSlide.markers.forEach((marker, index) => {
680        const markerEl = document.createElement('div');
681        markerEl.className = `builder-edit-marker ${marker.id === selectedMarkerId ? 'active' : ''}`;
682        markerEl.style.setProperty('--x', marker.x);
683        markerEl.style.setProperty('--y', marker.y);
684        markerEl.setAttribute('data-id', marker.id);
685        markerEl.setAttribute('data-number', index + 1);
686        
687        // Обработчик клика для выбора точки
688        markerEl.addEventListener('click', (e) => {
689            e.stopPropagation();
690            if (isDragging) return;
691            selectMarker(marker.id);
692        });
693        
694        // Drag-and-drop мыши
695        markerEl.addEventListener('mousedown', (e) => {
696            e.stopPropagation();
697            startDragging(marker.id, e);
698        });
699        
700        // Drag-and-drop Touch
701        markerEl.addEventListener('touchstart', (e) => {
702            e.stopPropagation();
703            startDragging(marker.id, e.touches[0]);
704        }, { passive: true });
705        
706        builderMarkersContainer.appendChild(markerEl);
707    });
708}
709
710// Выбор маркера в боковой панели
711function selectMarker(id) {
712    if (typeof clearImportStatus === 'function') clearImportStatus();
713    selectedMarkerId = id;
714    const currentSlide = slides[currentSlideIndex];
715    const marker = currentSlide.markers.find(m => m.id === id);
716    
717    if (!marker) {
718        markerEditorSection.style.display = 'none';
719        return;
720    }
721    
722    // Подсвечиваем активный маркер на холсте
723    const markerEls = builderMarkersContainer.querySelectorAll('.builder-edit-marker');
724    markerEls.forEach(el => {
725        el.classList.toggle('active', el.getAttribute('data-id') === id);
726    });
727    
728    // Заполняем форму маркера
729    markerEditorSection.style.display = 'block';
730    markerCoordX.textContent = marker.x;
731    markerCoordY.textContent = marker.y;
732    inputMarkerTitle.value = marker.title || '';
733    inputMarkerPrice.value = marker.price ? marker.price.replace(/\s/g, '') : '0';
734    inputMarkerDiscount.value = marker.discount || 0;
735    inputMarkerLink.value = marker.link || '';
736    
737    updateCalculatedPrice(marker);
738}
739
740// Расчет и вывод цены со скидкой
741function updateCalculatedPrice(marker) {
742    const rawPrice = inputMarkerPrice.value.replace(/\s/g, '');
743    const priceNum = parseFloat(rawPrice) || 0;
744    const discountNum = parseInt(inputMarkerDiscount.value) || 0;
745    
746    let finalPrice = priceNum;
747    if (discountNum > 0) {
748        finalPrice = Math.round(priceNum * (1 - discountNum / 100));
749    }
750    
751    calculatedPriceBadge.textContent = `${formatPrice(finalPrice)} ₽`;
752    
753    // Сохраняем в объект данных
754    marker.price = formatPrice(priceNum);
755    marker.discount = discountNum;
756    marker.priceWithDiscount = formatPrice(finalPrice);
757    
758    updateExportCode();
759}
760
761// Добавление новой точки кликом по холсту
762safeAddListener(builderCanvas, 'click', (e) => {
763    // Убедимся, что мы кликаем именно по холсту или картинке, а не по существующей точке
764    if (e.target.classList.contains('builder-edit-marker')) return;
765    
766    const currentSlide = slides[currentSlideIndex];
767    if (!currentSlide) return;
768    
769    const rect = builderCanvas.getBoundingClientRect();
770    let x = ((e.clientX - rect.left) / rect.width) * 100;
771    let y = ((e.clientY - rect.top) / rect.height) * 100;
772    
773    // Округляем до 1 знака после запятой
774    x = Math.round(x * 10) / 10;
775    y = Math.round(y * 10) / 10;
776    
777    // Ограничение границ
778    x = Math.max(1, Math.min(99, x));
779    y = Math.max(1, Math.min(99, y));
780    
781    const newMarker = {
782        id: 'marker_' + Date.now(),
783        x: x,
784        y: y,
785        title: 'Новый товар',
786        price: '5 000',
787        discount: 0,
788        priceWithDiscount: '5 000',
789        link: '/catalog/'
790    };
791    
792    pushHistory();
793    currentSlide.markers.push(newMarker);
794    
795    renderBuilderMarkers();
796    selectMarker(newMarker.id);
797    updateExportCode();
798});
799
800// Drag and drop логика
801function startDragging(markerId, pointerEvent) {
802    stateBeforeDrag = JSON.stringify(slides);
803    isDragging = true;
804    dragMarkerId = markerId;
805    selectMarker(markerId);
806    document.body.style.cursor = 'grabbing';
807}
808
809function handleDragging(pointerEvent) {
810    if (!isDragging || !dragMarkerId) return;
811    
812    const currentSlide = slides[currentSlideIndex];
813    const marker = currentSlide.markers.find(m => m.id === dragMarkerId);
814    if (!marker) return;
815    
816    const rect = builderCanvas.getBoundingClientRect();
817    let x = ((pointerEvent.clientX - rect.left) / rect.width) * 100;
818    let y = ((pointerEvent.clientY - rect.top) / rect.height) * 100;
819    
820    x = Math.round(x * 10) / 10;
821    y = Math.round(y * 10) / 10;
822    
823    x = Math.max(1, Math.min(99, x));
824    y = Math.max(1, Math.min(99, y));
825    
826    // Обновляем данные маркера
827    marker.x = x;
828    marker.y = y;
829    
830    // Быстро двигаем DOM-элемент
831    const markerEl = builderMarkersContainer.querySelector(`.builder-edit-marker[data-id="${dragMarkerId}"]`);
832    if (markerEl) {
833        markerEl.style.setProperty('--x', x);
834        markerEl.style.setProperty('--y', y);
835    }
836    
837    // Обновляем координаты в форме
838    markerCoordX.textContent = x;
839    markerCoordY.textContent = y;
840}
841
842function stopDragging() {
843    if (isDragging) {
844        isDragging = false;
845        dragMarkerId = null;
846        document.body.style.cursor = 'default';
847        
848        // Проверяем, изменились ли координаты
849        const currentStateStr = JSON.stringify(slides);
850        if (stateBeforeDrag && stateBeforeDrag !== currentStateStr) {
851            undoStack.push(stateBeforeDrag);
852            redoStack = [];
853            updateHistoryButtons();
854        }
855        stateBeforeDrag = null;
856        
857        updateExportCode();
858    }
859}
860
861// Привязка обработчиков перемещения мыши на весь док
861умент для плавности
862document.addEventListener('mousemove', handleDragging);
863document.addEventListener('mouseup', stopDragging);
864
865document.addEventListener('touchmove', (e) => {
866    if (isDragging) {
867        // e.preventDefault(); // Предотвращаем скролл страницы во время перетаскивания
868        handleDragging(e.touches[0]);
869    }
870}, { passive: false });
871document.addEventListener('touchend', stopDragging);
872
873// ==========================================================================
874// 5. Обработка изменений в формах
875// ==========================================================================
876
877// Переключение выбранного слайда
878safeAddListener(slideSelect, 'change', (e) => {
879    currentSlideIndex = parseInt(e.target.value);
880    selectedMarkerId = null;
881    initBuilder();
882});
883
884// Добавление слайда
885safeAddListener(addSlideBtn, 'click', () => {
886    pushHistory();
887    const newSlide = {
888        id: 'slide_' + Date.now(),
889        title: 'Искусство интерьера: Взгляд Виктора',
890        name: 'Новая коллекция мебели',
891        description: 'Описание новой коллекции: функциональная и красивая мебель для вашего уюта.',
892        image: 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1200&q=80',
893        link: '/catalog/',
894        badge: 'СОВЕТЫ ДИЗАЙНЕРА',
895        designerTitle: 'Искусство интерьера',
896        designer: {
897            name: 'Виктор',
898            role: 'Ведущий дизайнер',
899            avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=150&q=80'
900        },
901        markers: []
902    };
903    
904    slides.push(newSlide);
905    currentSlideIndex = slides.length - 1;
906    selectedMarkerId = null;
907    initBuilder();
908});
909
910// Удаление слайда
911safeAddListener(deleteSlideBtn, 'click', () => {
912    if (slides.length <= 1) {
913        alert('Нельзя удалить единственный слайд!');
914        return;
915    }
916    
917    if (confirm('Вы действительно хотите удалить этот слайд?')) {
918        pushHistory();
919        slides.splice(currentSlideIndex, 1);
920        currentSlideIndex = Math.max(0, currentSlideIndex - 1);
921        selectedMarkerId = null;
922        initBuilder();
923    }
924});
925
926// События форм редактирования текущего слайда
927safeAddListener(inputSlideName, 'input', (e) => {
928    if (slides[currentSlideIndex]) {
929        slides[currentSlideIndex].name = e.target.value;
930        // Обновляем имя слайда в списке выбора
931        if (slideSelect) {
932            const opt = slideSelect.options[currentSlideIndex];
933            if (opt) opt.textContent = `${currentSlideIndex + 1}. ${e.target.value}`;
934        }
935        // Обновляем в предпросмотре
936        slideCollectionName.textContent = e.target.value;
937        updateExportCode();
938    }
939});
940
941safeAddListener(inputSlideDesc, 'input', (e) => {
942    if (slides[currentSlideIndex]) {
943        slides[currentSlideIndex].description = e.target.value;
944        // Обновляем в предпросмотре
945        slideCollectionDesc.textContent = e.target.value;
946        updateExportCode();
947    }
948});
949
950safeAddListener(inputSlideImage, 'input', (e) => {
951    if (slides[currentSlideIndex]) {
952        slides[currentSlideIndex].image = e.target.value;
953        if (builderImagePreview) {
954            builderImagePreview.src = e.target.value || 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1200&q=80';
955        }
956        // Обновляем картинку активного слайда в предпросмотре
957        const activeSlideItem = sliderWrapper.querySelector('.slider-item.active img');
958        if (activeSlideItem) {
959            activeSlideItem.src = e.target.value || 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1200&q=80';
960        }
961        updateExportCode();
962    }
963});
964
965safeAddListener(inputSlideLink, 'input', (e) => {
966    if (slides[currentSlideIndex]) {
967        slides[currentSlideIndex].link = e.target.value;
968        // Обновляем в предпросмотре
969        slideCollectionLink.href = e.target.value || '#';
970        updateExportCode();
971    }
972});
973
974safeAddListener(inputSlideBadge, 'input', (e) => {
975    if (slides[currentSlideIndex]) {
976        slides[currentSlideIndex].badge = e.target.value;
977        // Обновляем в предпросмотре
978        slideBadge.textContent = e.target.value || 'СОВЕТЫ ДИЗАЙНЕРА';
979        updateExportCode();
980    }
981});
982
983safeAddListener(inputSlideDesignerTitle, 'input', (e) => {
984    if (slides[currentSlideIndex]) {
985        slides[currentSlideIndex].designerTitle = e.target.value;
986        // Обновляем в предпросмотре
987        designerTitleLabel.textContent = e.target.value || 'Искусство интерьера';
988        updateExportCode();
989    }
990});
991
992safeAddListener(inputDesignerName, 'input', (e) => {
993    if (slides[currentSlideIndex]) {
994        if (!slides[currentSlideIndex].designer) slides[currentSlideIndex].designer = {};
995        slides[currentSlideIndex].designer.name = e.target.value;
996        // Обновляем в предпросмотре
997        designerName.textContent = e.target.value || 'Виктор';
998        updateExportCode();
999    }
1000});
1001
1002safeAddListener(inputDesignerAvatar, 'input', (e) => {
1003    if (slides[currentSlideIndex]) {
1004        if (!slides[currentSlideIndex].designer) slides[currentSlideIndex].designer = {};
1005        slides[currentSlideIndex].designer.avatar = e.target.value;
1006        // Обновляем в предпросмотре
1007        designerImg.src = e.target.value || 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=150&q=80';
1008        updateExportCode();
1009    }
1010});
1011
1012// События форм редактирования товара (точки)
1013safeAddListener(inputMarkerTitle, 'input', (e) => {
1014    const currentSlide = slides[currentSlideIndex];
1015    if (currentSlide && selectedMarkerId) {
1016        const marker = currentSlide.markers.find(m => m.id === selectedMarkerId);
1017        if (marker) {
1018            marker.title = e.target.value;
1019            updateExportCode();
1020        }
1021    }
1022});
1023
1024safeAddListener(inputMarkerPrice, 'input', () => {
1025    const currentSlide = slides[currentSlideIndex];
1026    if (currentSlide && selectedMarkerId) {
1027        const marker = currentSlide.markers.find(m => m.id === selectedMarkerId);
1028        if (marker) {
1029            updateCalculatedPrice(marker);
1030        }
1031    }
1032});
1033
1034safeAddListener(inputMarkerDiscount, 'input', () => {
1035    const currentSlide = slides[currentSlideIndex];
1036    if (currentSlide && selectedMarkerId) {
1037        const marker = currentSlide.markers.find(m => m.id === selectedMarkerId);
1038        if (marker) {
1039            // Ограничение диапазона скидки
1040            let disc = parseInt(inputMarkerDiscount.value) || 0;
1041            if (disc < 0) {
1042                disc = 0;
1043                inputMarkerDiscount.value = 0;
1044            }
1045            if (disc > 99) {
1046                disc = 99;
1047                inputMarkerDiscount.value = 99;
1048            }
1049            updateCalculatedPrice(marker);
1050        }
1051    }
1052});
1053
1054safeAddListener(inputMarkerLink, 'input', (e) => {
1055    if (typeof clearImportStatus === 'function') clearImportStatus();
1056    const currentSlide = slides[currentSlideIndex];
1057    if (currentSlide && selectedMarkerId) {
1058        const marker = currentSlide.markers.find(m => m.id === selectedMarkerId);
1059        if (marker) {
1060            marker.link = e.target.value;
1061            updateExportCode();
1062        }
1063    }
1064});
1065
1066// --- Логика автозаполнения данных товара по ссылке (Scraping) ---
1067
1068function clearImportStatus() {
1069    importStatusMsg.style.display = 'none';
1070    importStatusMsg.className = 'import-status-message';
1071    importStatusMsg.innerHTML = '';
1072}
1073
1074function showImportStatus(text, type) {
1075    importStatusMsg.style.display = 'block';
1076    importStatusMsg.className = `import-status-message ${type}`;
1077    if (type === 'loading') {
1078        importStatusMsg.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> ${text}`;
1079    } else if (type === 'success') {
1080        importStatusMsg.innerHTML = `<i class="fa-solid fa-circle-check"></i> ${text}`;
1081    } else {
1082        importStatusMsg.innerHTML = `<i class="fa-solid fa-circle-exclamation"></i> ${text}`;
1083    }
1084}
1085
1086function makeAbsoluteUrl(url) {
1087    if (!url) return '';
1088    url = url.trim();
1089    if (url.startsWith('http://') || url.startsWith('https://')) {
1090        return url;
1091    }
1092    // Если это относительный путь
1093    const baseUrl = 'https://alleyadoma.ru';
1094    if (url.startsWith('/')) {
1095        return baseUrl + url;
1096    }
1097    return baseUrl + '/' + url;
1098}
1099
1100async function fetchProductHTML(url) {
1101    // 0. Если это локальный тестовый файл (например, product1.html или ./product1.html)
1102    const isLocalTest = url.endsWith('.html') && !url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('//');
1103    if (isLocalTest) {
1104        try {
1105            console.log("Попытка 0: Загрузка локального тестового файла...", url);
1106            const response = await fetch(url);
1107            if (response.ok) {
1108                const text = await response.text();
1109                if (text && text.trim().length > 0) {
1110                    console.log("Успешно загружен локальный тестовый файл");
1111                    return text;
1112                }
1113            }
1114        } catch (e) {
1115            console.warn("Не удалось загрузить локальный файл напрямую:", e);
1116        }
1117    }
1118
1119    // 0.5. Если мы находимся на том же домене, пробуем прямой fetch (браузер автоматически прикрепит куки авторизации пользователя)
1120    try {
1121        const currentHost = window.location.hostname;
1122        const targetUrlObj = new URL(url);
1123        const targetHost = targetUrlObj.hostname.replace('www.', '');
1124        const cleanCurrentHost = currentHost.replace('www.', '');
1125        
1126        if (cleanCurrentHost === targetHost || cleanCurrentHost.endsWith('.' + targetHost) || targetHost.endsWith('.' + cleanCurrentHost)) {
1127            console.log("Попытка 0.5: Прямой запрос к сайту (один домен)...");
1128            const response = await fetch(url);
1129            if (response.ok) {
1130                const text = await response.text();
1131                if (text && text.trim().length > 0) {
1132                    console.log("Успешно загружено напрямую через браузер (один домен)");
1133                    return text;
1134                }
1135            }
1136        }
1137    } catch (e) {
1138        console.log("Прямой запрос в рамках одного домена не удался (возможно, CORS при локальной разработке), пробуем прокси...");
1139    }
1140
1141    const encodedUrl = encodeURIComponent(url);
1142
1143    // 1. Сначала пробуем локальный PHP-прокси (для живого сайта с PHP)
1144    try {
1145        console.log("Попытка 1: Локальный PHP-прокси...");
1146        const response = await fetch(`get-product.php?url=${encodedUrl}`);
1147        if (response.ok) {
1148            const data = await response.json();
1149            if (data.contents) {
1150                console.log("Успешно загружено через локальный PHP-прокси");
1151                return data.contents;
1152            }
1153            if (data.error) console.warn("PHP Proxy error:", data.error);
1154        }
1155    } catch (e) {
1156        console.log("Локальный PHP-прокси недоступен (возможно, запуск без веб-сервера), пробуем публичные CORS-прокси...");
1157    }
1158
1159    // Список публичных прокси-серверов для каскадного опроса
1160    // Некоторые прокси возвращают сырой HTML, некоторые - JSON с полем contents.
1161    const publicProxies = [
1162        {
1163            name: "corsproxy.io",
1164            url: `https://corsproxy.io/?url=${encodedUrl}`,
1165            type: "raw"
1166        },
1167        {
1168            name: "codetabs.com",
1169            url: `https://api.codetabs.com/v1/proxy?url=${encodedUrl}`,
1170            type: "raw"
1171        },
1172        {
1173            name: "allorigins.win",
1174            url: `https://api.allorigins.win/get?url=${encodedUrl}`,
1175            type: "json"
1176        }
1177    ];
1178
1179    for (const proxy of publicProxies) {
1180        try {
1181            console.log(`Попытка загрузки через ${proxy.name}...`);
1182            const response = await fetch(proxy.url);
1183            if (response.ok) {
1184                if (proxy.type === "raw") {
1185                    const text = await response.text();
1186                    if (text && text.trim().length > 0) {
1187                        console.log(`Успешно загружено через ${proxy.name}`);
1188                        return text;
1189                    }
1190                } else if (proxy.type === "json") {
1191                    const data = await response.json();
1192                    if (data.contents) {
1193                        console.log(`Успешно загружено через ${proxy.name}`);
1194                        return data.contents;
1195                    }
1196                }
1197            } else {
1198                console.warn(`${proxy.name} вернул статус-код ${response.status}`);
1199            }
1200        } catch (err) {
1201            console.warn(`Ошибка при запросе к ${proxy.name}:`, err);
1202        }
1203    }
1204
1205    // Если все попытки провалились, выводим подробное руководство по исправлению
1206    throw new Error(
1207        "Не удалось загрузить данные товара.\n\n" +
1208        "Возможные причины и решения:\n" +
1209        "1. Если вы запустили конструктор локально через двойной клик по файлу (протокол file://), браузер блокирует запросы безопасности. Запустите локальный веб-сервер (например, через расширение Live Server в VS Code, или используя Node.js, Python, Open Server).\n" +
1210        "2. Если конструктор загружен на хостинг, убедитесь, что файл get-product.php находится в той же директории, что и index.html, и на сервере работает PHP.\n" +
1211        "3. Публичные CORS-прокси временно перегружены или блокируются вашим провайдером. Попробуйте еще раз через несколько минут."
1212    );
1213}
1214
1215function parseProductHTML(htmlString) {
1216    const parser = new DOMParser();
1217    const doc = parser.parseFromString(htmlString, 'text/html');
1218
1219    // 1. Заголовок товара
1220    const titleEl = doc.querySelector('h1[itemprop="name"]') || 
1221                    doc.querySelector('.right-inside-tovar h1') || 
1222                    doc.querySelector('h1') || 
1223                    doc.querySelector('.card-product-title');
1224    if (!titleEl) throw new Error("Не удалось найти название товара на странице. Убедитесь, что ссылка ведет на карточку товара.");
1225    const title = titleEl.textContent.trim().replace(/\s+/g, ' ');
1226
1227    // 2. Текущая цена (с учетом скидки)
1228    const metaPriceEl = doc.querySelector('meta[itemprop="price"]');
1229    let currentPrice = 0;
1230    if (metaPriceEl) {
1231        currentPrice = parseFloat(metaPriceEl.getAttribute('content')) || 0;
1232    }
1233    
1234    if (!currentPrice) {
1235        const priceTextEl = doc.querySelector('.fs-32.font-weight-700') || 
1236                            doc.querySelector('.actual-cost-inside .fs-32') ||
1237                            doc.querySelector('[data-entity="panel-price"]');
1238        if (priceTextEl) {
1239            currentPrice = parseFloat(priceTextEl.textContent.replace(/\s/g, '').replace(/&nbsp;/g, '').replace(/\u00a0/g, '').replace(/\D/g, '')) || 0;
1240        }
1241    }
1242
1243    if (!currentPrice) {
1244        throw new Error("Не удалось извлечь цену товара. Возможно, товара нет в наличии или изменилась структура страницы.");
1245    }
1246
1247    // 3. Старая цена (цена без скидки)
1248    const oldPriceEl = doc.querySelector('.old-cost-inside') || 
1249                       doc.querySelector('.price-old') || 
1250                       doc.querySelector('.old-price');
1251    let oldPrice = currentPrice;
1252    if (oldPriceEl) {
1253        oldPrice = parseFloat(oldPriceEl.textContent.replace(/\s/g, '').replace(/&nbsp;/g, '').replace(/\u00a0/g, '').replace(/\D/g, '')) || currentPrice;
1254    }
1255
1256    // 4. Процент скидки
1257    let discount = 0;
1258    const discountEl = doc.querySelector('.discount') || doc.querySelector('.percent');
1259    if (discountEl) {
1260        discount = parseInt(discountEl.textContent.replace(/\s/g, '').replace(/\D/g, '')) || 0;
1261    } else if (oldPrice > currentPrice) {
1262        discount = Math.round(((oldPrice - currentPrice) / oldPrice) * 100);
1263    }
1264
1265    return {
1266        title,
1267        price: oldPrice,
1268        discount,
1269        priceWithDiscount: currentPrice
1270    };
1271}
1272
1273safeAddListener(fetchProductBtn, 'click', async () => {
1274    const currentSlide = slides[currentSlideIndex];
1275    if (!currentSlide || !selectedMarkerId) return;
1276
1277    const marker = currentSlide.markers.find(m => m.id === selectedMarkerId);
1278    if (!marker) return;
1279
1280    const urlInput = inputMarkerLink.value.trim();
1281    if (!urlInput) {
1282        showImportStatus("Пожалуйста, сначала введите ссылку на товар.", "error");
1283        return;
1284    }
1285
1286    const isLocalTest = urlInput.endsWith('.html') && !urlInput.startsWith('http://') && !urlInput.startsWith('https://') && !urlInput.startsWith('//');
1287    const absoluteUrl = isLocalTest ? urlInput : makeAbsoluteUrl(urlInput);
1288    if (!isLocalTest && !absoluteUrl.includes('alleyadoma.ru')) {
1289        showImportStatus("Разрешены только ссылки с сайта alleyadoma.ru", "error");
1290        return;
1291    }
1292
1293    try {
1294        fetchProductBtn.disabled = true;
1295        const btnText = fetchProductBtn.querySelector('span');
1296        const btnIcon = fetchProductBtn.querySelector('i');
1297        
1298        const originalText = btnText.textContent;
1299        btnText.textContent = "Загрузка...";
1300        btnIcon.className = "fa-solid fa-spinner fa-spin";
1301        
1302        showImportStatus("Загрузка данных о товаре...", "loading");
1303
1304        const html = await fetchProductHTML(absoluteUrl);
1305        const productData = parseProductHTML(html);
1306
1307        // Обновляем форму в боковой панели
1308        inputMarkerTitle.value = productData.title;
1309        inputMarkerPrice.value = formatPrice(productData.price);
1310        inputMarkerDiscount.value = productData.discount;
1311        
1312        // Превращаем ссылку обратно в относительную для красоты
1313        let cleanLink = urlInput;
1314        if (cleanLink.startsWith('http://') || cleanLink.startsWith('https://')) {
1315            try {
1316                const parsed = new URL(cleanLink);
1317                cleanLink = parsed.pathname + parsed.search + parsed.hash;
1318            } catch (e) {}
1319        }
1320        inputMarkerLink.value = cleanLink;
1321
1322        // Сохраняем в маркер и пересчитываем цену со скидкой
1323        pushHistory();
1324        marker.title = productData.title;
1325        marker.link = cleanLink;
1326        updateCalculatedPrice(marker);
1327
1328        // Перерисовываем карусель и холст, чтобы увидеть изменения мгновенно
1329        renderBuilderMarkers();
1330        updateExportCode();
1331
1332        showImportStatus("Данные товара успешно импортированы!", "success");
1333        
1334        // Возвращаем кнопку в исходное состояние
1335        btnText.textContent = originalText;
1336        btnIcon.className = "fa-solid fa-cloud-arrow-down";
1337        fetchProductBtn.disabled = false;
1338
1339    } catch (error) {
1340        console.error("Ошибка при импорте данных товара:", error);
1341        showImportStatus(error.message || "Произошла неизвестная ошибка при парсинге страницы.", "error");
1342        
1343        // Возвращаем кнопку в исходное состояние
1344        const btnText = fetchProductBtn.querySelector('span');
1345        const btnIcon = fetchProductBtn.querySelector('i');
1346        btnText.textContent = "Импортировать данные";
1347        btnIcon.className = "fa-solid fa-cloud-arrow-down";
1348        fetchProductBtn.disabled = false;
1349    }
1350});
1351
1352// Удаление маркера
1353safeAddListener(deleteMarkerBtn, 'click', () => {
1354    const currentSlide = slides[currentSlideIndex];
1355    if (currentSlide && selectedMarkerId) {
1356        pushHistory();
1357        currentSlide.markers = currentSlide.markers.filter(m => m.id !== selectedMarkerId);
1358        selectedMarkerId = null;
1359        if (markerEditorSection) markerEditorSection.style.display = 'none';
1360        renderBuilderMarkers();
1361        updateExportCode();
1362    }
1363});
1364
1365// ==========================================================================
1366// 6. Экспорт и генерация кода
1367// ==========================================================================
1368
1369if (exportTabButtons && exportTabButtons.length > 0) {
1370    exportTabButtons.forEach(btn => {
1371        btn.addEventListener('click', () => {
1372            exportTabButtons.forEach(b => b.classList.remove('active'));
1373            btn.classList.add('active');
1374            activeExportType = btn.getAttribute('data-export');
1375            updateExportCode();
1376        });
1377    });
1378}
1379
1380function updateExportCode() {
1381    saveToLocalStorage();
1382    if (!exportTextarea) return;
1383    if (activeExportType === 'json') {
1384        // Форматированный JSON с отступами
1385        exportTextarea.value = JSON.stringify(slides, null, 4);
1386    } else {
1387        // Генерируем красивый HTML-шаблон для вставки на сайт
1388        let htmlCode = `<!-- Блок «Советы дизайнера» для alleyadoma.ru -->\n`;
1389        htmlCode += `<div class="designer-advice__grid">\n`;
1390        htmlCode += `    <div class="designer-advice__media">\n`;
1391        htmlCode += `        <div class="designer-advice-slider">\n`;
1392        htmlCode += `            <div class="slider-wrapper">\n`;
1393        
1394        slides.forEach((slide, sIdx) => {
1395            const activeClass = sIdx === 0 ? ' active' : '';
1396            htmlCode += `                <!-- Слайд ${sIdx + 1}: ${slide.name} -->\n`;
1397            htmlCode += `                <div class="slider-item${activeClass}" data-slide-id="${slide.id}" \n`;
1398            htmlCode += `                     data-slide-badge="${slide.badge || 'СОВЕТЫ ДИЗАЙНЕРА'}" \n`;
1399            htmlCode += `                     data-designer-title="${slide.designerTitle || 'Искусство интерьера'}" \n`;
1400            htmlCode += `                     data-designer-name="${slide.designer?.name || 'Виктор'}" \n`;
1401            htmlCode += `                     data-designer-avatar="${slide.designer?.avatar || ''}" \n`;
1402            htmlCode += `                     data-collection-name="${slide.name || ''}" \n`;
1403            htmlCode += `                     data-collection-desc="${slide.description || ''}" \n`;
1404            htmlCode += `                     data-collection-link="${slide.link || ''}">\n`;
1405            htmlCode += `                    <img src="${slide.image}" alt="${slide.name}">\n`;
1406            htmlCode += `                    <div class="complect-markers">\n`;
1407            
1408            slide.markers.forEach((marker, mIdx) => {
1409                const discountAttr = marker.discount > 0 ? ` data-discount="${marker.discount}"` : '';
1410                const oldPriceAttr = marker.discount > 0 ? ` data-old-price="${marker.price}"` : '';
1411                
1412                htmlCode += `                        <div class="complect-marker" style="--x: ${marker.x}; --y: ${marker.y};" \n`;
1413                htmlCode += `                             data-title="${marker.title}" \n`;
1414                htmlCode += `                             data-price="${marker.priceWithDiscount || marker.price}"${discountAttr}${oldPriceAttr} \n`;
1415                htmlCode += `                             data-link="${marker.link}">\n`;
1416                htmlCode += `                            <span data-href="${marker.link}" class="complect-marker__btn"></span>\n`;
1417                htmlCode += `                        </div>\n`;
1418            });
1419            
1420            htmlCode += `                    </div>\n`;
1421            htmlCode += `                </div>\n`;
1422        });
1423        
1424        htmlCode += `            </div>\n`;
1425        htmlCode += `            <!-- Кнопки управления -->\n`;
1426        htmlCode += `            <button class="slider-btn prev" aria-label="Предыдущий"><i class="fa-solid fa-chevron-left"></i></button>\n`;
1427        htmlCode += `            <button class="slider-btn next" aria-label="Следующий"><i class="fa-solid fa-chevron-right"></i></button>\n`;
1428        htmlCode += `            <div class="slider-pagination"></div>\n`;
1429        htmlCode += `        </div>\n`;
1430        htmlCode += `    </div>\n\n`;
1431        
1432        // Правая колонка дизайнера
1433        htmlCode += `    <div class="designer-advice__info">\n`;
1434        htmlCode += `        <div class="designer-advice__badge">${slides[0]?.badge || 'СОВЕТЫ ДИЗАЙНЕРА'}</div>\n`;
1435        htmlCode += `        <div class="designer-card">\n`;
1436        htmlCode += `            <div class="designer-avatar">\n`;
1437        htmlCode += `                <img src="${slides[0]?.designer?.avatar || ''}" class="designer-avatar-img" alt="Аватар">\n`;
1438        htmlCode += `            </div>\n`;
1439        htmlCode += `            <div class="designer-details">\n`;
1440        htmlCode += `                <span class="designer-title">${slides[0]?.designerTitle || 'Искусство интерьера'}</span>\n`;
1441        htmlCode += `                <span class="designer-name" id="widget-designer-name">${slides[0]?.designer?.name || ''}</span>\n`;
1442        htmlCode += `            </div>\n`;
1443        htmlCode += `        </div>\n`;
1444        htmlCode += `        <div class="designer-advice__content">\n`;
1445        htmlCode += `            <h3 class="designer-advice__content-title" id="widget-collection-name">${slides[0]?.name || ''}</h3>\n`;
1446        htmlCode += `            <p class="designer-advice__content-desc" id="widget-collection-desc">${slides[0]?.description || ''}</p>\n`;
1447        htmlCode += `        </div>\n`;
1448
1449        htmlCode += `    </div>\n`;
1450        htmlCode += `</div>\n`;
1451        
1452        exportTextarea.value = htmlCode;
1453    }
1454}
1455
1456// Копирование кода в буфер обмена
1457safeAddListener(copyCodeBtn, 'click', () => {
1458    if (exportTextarea) {
1459        exportTextarea.select();
1460        document.execCommand('copy');
1461    }
1462    
1463    // Визуальное подтверждение
1464    const originalText = copyCodeBtn.innerHTML;
1465    copyCodeBtn.innerHTML = '<i class="fa-solid fa-check"></i> Скопировано!';
1466    copyCodeBtn.classList.remove('btn-secondary');
1467    copyCodeBtn.classList.add('btn-primary');
1468    
1469    setTimeout(() => {
1470        copyCodeBtn.innerHTML = originalText;
1471        copyCodeBtn.classList.remove('btn-primary');
1472        copyCodeBtn.classList.add('btn-secondary');
1473    }, 2000);
1474});
1475
1476// Сброс локального хранилища (возврат к исходным данным)
1477const resetStorageBtn = document.getElementById('reset-storage-btn');
1478if (resetStorageBtn) {
1479    resetStorageBtn.addEventListener('click', () => {
1480        if (confirm("Вы действительно хотите сбросить все ваши изменения и вернуться к демонстрационным данным по умолчанию?")) {
1481            localStorage.removeItem(STORAGE_KEY);
1482            location.reload();
1483        }
1484    });
1485}
1486
1487// ==========================================================================
1488// 6. Старт приложения и инициализация новых функций
1489// ==========================================================================
1490
1491// Инициализация свайпов для мобильных
1492let touchStartX = 0;
1493let touchStartY = 0;
1494let touchEndX = 0;
1495let touchEndY = 0;
1496
1497function initSwipeAndHover() {
1498    const sliderContainer = document.querySelector('.designer-advice-slider');
1499    if (sliderContainer) {
1500        // Жесты свайпа
1501        sliderContainer.addEventListener('touchstart', (e) => {
1502            touchStartX = e.changedTouches[0].clientX;
1503            touchStartY = e.changedTouches[0].clientY;
1504        }, { passive: true });
1505
1506        sliderContainer.addEventListener('touchend', (e) => {
1507            touchEndX = e.changedTouches[0].clientX;
1508            touchEndY = e.changedTouches[0].clientY;
1509            handleSwipe();
1510        }, { passive: true });
1511        
1512        // Пауза автопрокрутки при наведении
1513        sliderContainer.addEventListener('mouseenter', () => {
1514            isMouseInsideSlider = true;
1515            pauseAutoplay();
1516        });
1517        sliderContainer.addEventListener('mouseleave', () => {
1518            isMouseInsideSlider = false;
1519            resumeAutoplay();
1520        });
1521    }
1522}
1523
1524function handleSwipe() {
1525    const diffX = touchEndX - touchStartX;
1526    const diffY = touchEndY - touchStartY;
1527    
1528    // Порог свайпа по горизонтали > 50px
1529    if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
1530        if (diffX > 0) {
1531            goToSlide(currentSlideIndex - 1);
1532        } else {
1533            goToSlide(currentSlideIndex + 1);
1534        }
1535    }
1536}
1537
1538// Загрузка файлов Drag-and-Drop с Canvas-сжатием
1539function compressImage(file, maxWidth, maxHeight, quality, callback) {
1540    const reader = new FileReader();
1541    reader.onload = function (e) {
1542        const img = new Image();
1543        img.onload = function () {
1544            const canvas = document.createElement('canvas');
1545            let width = img.width;
1546            let height = img.height;
1547            
1548            if (width > height) {
1549                if (width > maxWidth) {
1550                    height = Math.round((height * maxWidth) / width);
1551                    width = maxWidth;
1552                }
1553            } else {
1554                if (height > maxHeight) {
1555                    width = Math.round((width * maxHeight) / height);
1556                    height = maxHeight;
1557                }
1558            }
1559            
1560            canvas.width = width;
1561            canvas.height = height;
1562            
1563            const ctx = canvas.getContext('2d');
1564            ctx.drawImage(img, 0, 0, width, height);
1565            
1566            const dataUrl = canvas.toDataURL('image/jpeg', quality);
1567            callback(dataUrl);
1568        };
1569        img.src = e.target.result;
1570    };
1571    reader.readAsDataURL(file);
1572}
1573
1574function initDragAndDrop() {
1575    const canvas = document.getElementById('builder-canvas');
1576    const avatarZone = document.getElementById('avatar-drop-zone');
1577    const avatarInput = document.getElementById('avatar-file-input');
1578
1579    if (canvas) {
1580        canvas.addEventListener('dragover', (e) => {
1581            e.preventDefault();
1582            canvas.classList.add('drag-over');
1583        });
1584        
1585        canvas.addEventListener('dragleave', () => {
1586            canvas.classList.remove('drag-over');
1587        });
1588        
1589        canvas.addEventListener('drop', (e) => {
1590            e.preventDefault();
1591            canvas.classList.remove('drag-over');
1592            
1593            const files = e.dataTransfer.files;
1594            if (files && files.length > 0) {
1595                const file = files[0];
1596                if (file.type.startsWith('image/')) {
1597                    pushHistory();
1598                    compressImage(file, 1280, 800, 0.75, (base64) => {
1599                        const currentSlide = slides[currentSlideIndex];
1600                        if (currentSlide) {
1601                            currentSlide.image = base64;
1602                            if (inputSlideImage) inputSlideImage.value = base64;
1603                            if (builderImagePreview) builderImagePreview.src = base64;
1604                            
1605                            const activeSlideItem = sliderWrapper.querySelector('.slider-item.active img');
1606                            if (activeSlideItem) {
1607                                activeSlideItem.src = base64;
1608                            }
1609                            updateExportCode();
1610                        }
1611                    });
1612                }
1613            }
1614        });
1615    }
1616
1617    if (avatarZone && avatarInput) {
1618        avatarZone.addEventListener('click', () => {
1619            avatarInput.click();
1620        });
1621
1622        avatarInput.addEventListener('change', (e) => {
1623            const files = e.target.files;
1624            if (files && files.length > 0) {
1625                handleAvatarFile(files[0]);
1626            }
1627        });
1628
1629        avatarZone.addEventListener('dragover', (e) => {
1630            e.preventDefault();
1631            avatarZone.classList.add('drag-over');
1632        });
1633        
1634        avatarZone.addEventListener('dragleave', () => {
1635            avatarZone.classList.remove('drag-over');
1636        });
1637        
1638        avatarZone.addEventListener('drop', (e) => {
1639            e.preventDefault();
1640            avatarZone.classList.remove('drag-over');
1641            
1642            const files = e.dataTransfer.files;
1643            if (files && files.length > 0) {
1644                handleAvatarFile(files[0]);
1645            }
1646        });
1647    }
1648}
1649
1650function handleAvatarFile(file) {
1651    if (file.type.startsWith('image/')) {
1652        pushHistory();
1653        compressImage(file, 150, 150, 0.75, (base64) => {
1654            const currentSlide = slides[currentSlideIndex];
1655            if (currentSlide) {
1656                if (!currentSlide.designer) currentSlide.designer = {};
1657                currentSlide.designer.avatar = base64;
1658                if (inputDesignerAvatar) inputDesignerAvatar.value = base64;
1659                
1660                const previewImg = document.getElementById('avatar-drop-zone-preview');
1661                const icon = document.getElementById('avatar-drop-zone-icon');
1662                if (previewImg && icon) {
1663                    previewImg.src = base64;
1664                    previewImg.style.display = 'block';
1665                    icon.style.display = 'none';
1666                }
1667                
1668                designerImg.src = base64;
1669                updateExportCode();
1670            }
1671        });
1672    }
1673}
1674
1675// Инициализация истории ввода текстовых полей и кнопок Undo/Redo
1676function initHistoryHandlers() {
1677    const undoBtn = document.getElementById('undo-btn');
1678    const redoBtn = document.getElementById('redo-btn');
1679    
1680    if (undoBtn) undoBtn.addEventListener('click', undo);
1681    if (redoBtn) redoBtn.addEventListener('click', redo);
1682
1683    const inputs = [
1684        inputSlideName, inputSlideDesc, inputSlideImage, inputSlideLink, 
1685        inputSlideBadge, inputSlideDesignerTitle, inputDesignerName, inputDesignerAvatar,
1686        inputMarkerTitle, inputMarkerPrice, inputMarkerDiscount, inputMarkerLink
1687    ];
1688    
1689    inputs.forEach(input => {
1690        if (input) {
1691            input.addEventListener('focus', () => {
1692                stateBeforeInput = JSON.stringify(slides);
1693            });
1694            input.addEventListener('change', () => {
1695                const currentStateStr = JSON.stringify(slides);
1696                if (stateBeforeInput && stateBeforeInput !== currentStateStr) {
1697                    undoStack.push(stateBeforeInput);
1698                    redoStack = [];
1699                    updateHistoryButtons();
1700                    stateBeforeInput = currentStateStr;
1701                }
1702            });
1703        }
1704    });
1705
1706    // Горячие клавиши (Ctrl+Z / Ctrl+Y)
1707    document.addEventListener('keydown', (e) => {
1708        const builderTab = document.getElementById('builder-tab');
1709        if (!builderTab || !builderTab.classList.contains('active')) return;
1710        
1711        if (e.ctrlKey || e.metaKey) {
1712            const key = e.key.toLowerCase();
1713            if (key === 'z' || key === 'я') {
1714                e.preventDefault();
1715                undo();
1716            } else if (key === 'y' || key === 'н') {
1717                e.preventDefault();
1718                redo();
1719            }
1720        }
1721    });
1722}
1723
1724// Запуск при загрузке документа
1725window.addEventListener('DOMContentLoaded', () => {
1726    // 1. Инициализируем данные слайдов (слайды из data.js или localStorage)
1727    initSlidesData();
1728
1729    // 2. Инициализируем все переменные DOM
1730    initDOMElements();
1731
1732    // 3. Если слайдер на странице не найден, полностью прекращаем выполнение (чтобы скрипт не работал вхолостую)
1733    if (!sliderWrapper) {
1734        console.warn("[Designer tips] Контейнер слайдера не найден на странице. Скрипт остановлен.");
1735        return;
1736    }
1737
1738    // 4. Инициализируем привязку основных событий (клики по стрелочкам и табам)
1739    initMainEvents();
1740
1741    // 5. Запускаем рендеринг карусели
1742    renderSlider();
1743    
1744    // 6. Запускаем автопрокрутку
1745    startAutoplay();
1746    
1747    // 7. Инициализируем дополнительные обработчики
1748    initSwipeAndHover();
1749    initDragAndDrop();
1750    initHistoryHandlers();
1751    
1752    // 8. Логика ручного импорта HTML страницы товара (в конструкторе)
1753    const toggleManualImportBtn = document.getElementById('toggle-manual-import-btn');
1754    const manualImportContainer = document.getElementById('manual-html-import-container');
1755    const parseManualHtmlBtn = document.getElementById('parse-manual-html-btn');
1756    const manualHtmlInput = document.getElementById('manual-html-input');
1757
1758    if (toggleManualImportBtn && manualImportContainer) {
1759        toggleManualImportBtn.addEventListener('click', (e) => {
1760            e.preventDefault();
1761            const isHidden = manualImportContainer.style.display === 'none';
1762            manualImportContainer.style.display = isHidden ? 'block' : 'none';
1763            toggleManualImportBtn.textContent = isHidden ? 'Скрыть ручной ввод' : 'Или вставить исходный код вручную';
1764        });
1765    }
1766
1767    if (parseManualHtmlBtn && manualHtmlInput) {
1768        parseManualHtmlBtn.addEventListener('click', () => {
1769            const currentSlide = slides[currentSlideIndex];
1770            if (!currentSlide || !selectedMarkerId) {
1771                alert("Пожалуйста, выберите точку на холсте.");
1772                return;
1773            }
1774            const marker = currentSlide.markers.find(m => m.id === selectedMarkerId);
1775            if (!marker) return;
1776
1777            const htmlContent = manualHtmlInput.value.trim();
1778            if (!htmlContent) {
1779                alert("Пожалуйста, вставьте HTML-код страницы.");
1780                return;
1781            }
1782
1783            try {
1784                showImportStatus("Распаковка данных...", "loading");
1785                const productData = parseProductHTML(htmlContent);
1786
1787                // Заполняем форму
1788                if (inputMarkerTitle) inputMarkerTitle.value = productData.title;
1789                if (inputMarkerPrice) inputMarkerPrice.value = formatPrice(productData.price);
1790                if (inputMarkerDiscount) inputMarkerDiscount.value = productData.discount;
1791
1792                // Сохраняем в историю и маркер
1793                pushHistory();
1794                marker.title = productData.title;
1795                updateCalculatedPrice(marker);
1796
1797                renderBuilderMarkers();
1798                updateExportCode();
1799
1800                showImportStatus("Данные товара успешно импортированы вручную!", "success");
1801                manualHtmlInput.value = '';
1802                manualImportContainer.style.display = 'none';
1803                if (toggleManualImportBtn) toggleManualImportBtn.textContent = 'Или вставить исходный код вручную';
1804            } catch (err) {
1805                console.error("Ошибка ручной распаковки:", err);
1806                showImportStatus(err.message || "Ошибка парсинга HTML. Проверьте правильность вставленного кода.", "error");
1807            }
1808        });
1809    }
1810});

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.