Список пользователей

1
Админ
Постов: 101
2
Элита
Постов: 34
3
Элита
Постов: 28
4
VIP
Постов: 26
5
Дизайнер
Постов: 25
6
Пользователи
Постов: 25
7
Пользователи
Постов: 24
8
Проверенные
Постов: 21

  • Страница 1 из 1
  • 1
Парсер горячих тем - планшета со стилиусом финальная UCOZ
Дата: Суббота, 25.10.2025, 14:03 | Сообщение # 1 | | Написал: Начинающий
Автор темы
Мурчанн не в сети
        Сообщений:101
         Регистрация:20.10.2016

Общее назначение скрипта

Скрипт создаёт карусель «горячих» тем на форуме, автоматически подгружает новые темы и контент, очищает текст сообщений от лишнего мусора и спецсимволов, подставляет картинки и аватары.

Всё это делается невидимо для пользователя, без полной перезагрузки страницы.

Основные изменения и улучшения

Кэширование данных

CACHE_TIME увеличен до 30 минут (1800000 мс).

Скрипт сначала проверяет локальное хранилище (localStorage) и использует сохранённые данные, если они свежие.

Это сокращает количество запросов к серверу и ускоряет работу.

MIN_REFRESH_MS = 5000

Минимальная задержка между повторными запросами 5 секунд, чтобы не нагружать сервер.

Автообновление карусели

Используется setInterval(() loadData(false), 10000)

Каждые 10 секунд скрипт проверяет наличие новых «горячих» тем.

Если данные изменились — карусель плавно обновляется.

Пользователь не видит, что идёт подгрузка.

Чистка текста сообщений

Добавлена функция getCleanText(el, maxLength = 220):

Убирает табуляции, переносы строк, неразрывные пробелы.

Убирает лишние пробелы, пробелы перед знаками препинания.

Удаляет CSS-свойства, HTML-теги и лишние спецсимволы.

Ограничивает текст до 220 символов (maxLength).

Загрузка изображений и аватаров

Для каждой темы скрипт ищет:

Основное изображение темы (img в сообщении), пропуская аватары и служебные картинки.

Аватар автора, если есть.

Если картинок нет подставляется иконка по умолчанию.

Рендеринг карусели

Создаются карточки с изображением, заголовком, текстом, автором и счётчиком просмотров.

Карточки отображаются в плавной горизонтальной карусели.

Управление стрелками и свайпами:

Клик по стрелкам перемещает карусель.

Поддержка клавиш ArrowLeft и ArrowRight.

Свайпы на мобильных устройствах.

Оптимизация повторной загрузки

Скрипт сравнивает ссылки тем (lastRenderedHrefs), чтобы не перерисовывать карусель, если данные не изменились.

Это предотвращает лишние мерцания и работу DOM.

Что делает скрипт пошагово

1. Находит контейнер карусели и элементы управления (стрелки).

2. Проверяет кэш (localStorage) на наличие свежих данных:

3. Если есть сразу рендерит карусель.

4. Если нет или устарело подгружает с форума.

5. Парсит HTML страниц форума:

6. Находит «горячие» темы (badge «горячая» или страницы темы).

7. Берёт ссылку, заголовок, изображение, автора, количество просмотров.

Для каждой темы:

1. Загружает текст сообщения, очищает через getCleanText.

2. Находит главное изображение и аватар автора.

3. Собирает все данные в карточки и рендерит карусель.

4. Каждые 10 секунд проверяет новые данные и обновляет карусель невидимо.

5. Поддерживает адаптивный размер карточек при ресайзе окна.

Код
<script>
(async function nfForumCarouselHot3_fixedCarousel() {
  const track = document.querySelector('.nf-carousel-track');
  if (!track) return;

  const arrowLeft = document.querySelector('.nf-carousel-arrow-left');
  const arrowRight = document.querySelector('.nf-carousel-arrow-right');
  const container = track.closest('.nf-carousel-container') || track.parentElement;

const BASE_FORUM_URL = '/forum/0-0-1-34';
const CACHE_KEY = 'nf_forum_hot_3_clean';
const CACHE_TIME = 30 * 60 * 1000;   // 30 минут кэш
const MIN_REFRESH_MS = 5000;         // минимальная задержка 5 секунд
const MAX_CARDS = 16;                // макс. количество карточек

  let lastFetch = 0;
  let inProgress = false;
  let lastRenderedHrefs = '';
  let index = 0;
  let arrowsBound = false;

  function toAbs(url) {
    if (!url) return '';
    if (url.startsWith('http')) return url;
    if (url.startsWith('//')) return window.location.protocol + url;
    return window.location.origin + (url.startsWith('/') ? url : '/' + url);
  }

  function escapeHtml(s) {
    return String(s || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
  }

  // --- Чистый текст без спецсимволов и больших пробелов ---
function getCleanText(el, maxLength = 220) {
    if (!el) return '';
    let text = '';
    const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false);
    let node;
    while(node = walker.nextNode()) {
        // пропускаем скрипты, стили и iframe
        if(node.parentNode.closest('script, style, iframe')) continue;
        let t = node.nodeValue;
        if(!t) continue;

        // убираем табы, переносы строк, неразрывные пробелы
        t = t.replace(/[\t\n\r\u00A0]+/g, ' ');

        // убираем лишние пробелы
        t = t.replace(/\s+/g, ' ').trim();

        if(t) text += t + ' ';
    }

    // пробелы перед знаками препинания
    text = text.replace(/\s+([.,!?;:])/g, '$1');

    // полностью удаляем HTML-теги, если что-то осталось
    text = text.replace(/<[^>]+>/g, '');

    // удаляем CSS свойства и подобное
    text = text.replace(/\b(?:margin|padding|width|height|text-align|border-radius|box-shadow|color|background|font|line-height)\s*:[^;]+;/gi, '');

    // убираем все странные спецсимволы, оставляем буквы, цифры, базовую пунктуацию
    text = text.replace(/[^\wа-яА-Я0-9.,!?;: \-()'"«»]/g, '');

    // обрезаем до maxLength символов
    return text.trim().slice(0, maxLength);
}

  async function fetchHTML(url) {
    const res = await fetch(url, { credentials: 'same-origin', cache: 'no-cache' });
    return await res.text();
  }

  function findHotInDoc(doc) {
    const rows = Array.from(doc.querySelectorAll('td.threadNametd, .threadNametd, tr'));
    const result = [];
    for (let td of rows) {
      const badge = td.querySelector('.ipsBadge');
      const pages = td.querySelector('.postpSwithces, .postPSwithcesLink');
      if (!((badge && /горяч/i.test(badge.textContent || '')) || pages)) continue;
      const linkEl = td.querySelector('a.threadLink');
      if (!linkEl) continue;
      const href = toAbs(linkEl.getAttribute('href') || linkEl.href || '');
      const title = (linkEl.textContent || '').trim() || '(без названия)';
      const row = td.closest('tr');
      let icon = row?.querySelector('img')?.getAttribute('src') || '';
      icon = toAbs(icon) || '/forumimages/Newsletter-3.png';
      const author = (row?.querySelector('.threadAuthTd')?.textContent || '').trim() || 'Аноним';
      const views = parseInt((row?.querySelector('.threadViewTd, .views')?.textContent || '').replace(/\D/g, '')) || 0;
      result.push({ href, title, icon, author, views });
      if (result.length >= MAX_CARDS) break;
    }
    return result;
  }

  function uniqueThreads(arr) {
    const seen = new Set();
    return arr.filter(t => {
      if (seen.has(t.href)) return false;
      seen.add(t.href);
      return true;
    });
  }

  async function enrichThread(t) {
    try {
      const txt = await fetchHTML(t.href);
      const doc = new DOMParser().parseFromString(txt, 'text/html');
      const postEl = doc.querySelector('.post_content, .post_body, .post, .message, .ipsType_richText');
      const text = getCleanText(postEl);
      let img = '';
      if (postEl) {
        for (let im of Array.from(postEl.querySelectorAll('img'))) {
          const src = im.getAttribute('data-src') || im.src || '';
          if (!src) continue;
          if (/avatar|forumimages|template/i.test(src)) continue;
          img = toAbs(src); break;
        }
      }
      if (!img) img = t.icon;
      const authorEl = doc.querySelector('.postUser, .author, .post-author-name');
      const author = (authorEl?.textContent?.trim()) || t.author || 'Аноним';
      const avatarEl = doc.querySelector('.avatar img, .ipsUserPhoto');
      const avatar = toAbs(avatarEl?.src || '/forumimages/default-avatar.png');
      return { ...t, text, img, author, avatar };
    } catch (e) {
      return { ...t, text: '', img: t.icon, avatar: '/forumimages/default-avatar.png' };
    }
  }

  function setSlideSizes() {
    const slides = Array.from(track.children);
    const width = (container?.clientWidth || track.clientWidth || 600);
    slides.forEach(s => s.style.width = width + 'px');
    track.style.display = 'flex';
    track.style.transition = 'transform 0.45s ease';
  }

  function updateCarouselPosition() {
    const total = track.children.length || 1;
    if (index < 0) index = 0;
    if (index > total - 1) index = Math.max(0, total - 1);
    const width = (container?.clientWidth || track.clientWidth || 600);
    track.style.transform = `translateX(${-index * width}px)`;
    if (arrowLeft) arrowLeft.style.opacity = index === 0 ? '0.4' : '1';
    if (arrowRight) arrowRight.style.opacity = index >= total - 1 ? '0.4' : '1';
  }

  function bindArrowsOnce() {
    if (arrowsBound) return;
    arrowsBound = true;
    arrowLeft?.addEventListener('click', () => { index = Math.max(0, index - 1); updateCarouselPosition(); });
    arrowRight?.addEventListener('click', () => { index = Math.min(track.children.length - 1, index + 1); updateCarouselPosition(); });
    window.addEventListener('keydown', e => {
      if (e.key === 'ArrowLeft') index = Math.max(0, index - 1), updateCarouselPosition();
      if (e.key === 'ArrowRight') index = Math.min(track.children.length - 1, index + 1), updateCarouselPosition();
    });
    let startX = null;
    track.addEventListener('touchstart', e => startX = e.touches[0].clientX, { passive:true });
    track.addEventListener('touchend', e => {
      if (startX === null) return;
      const dx = e.changedTouches[0].clientX - startX;
      if (Math.abs(dx) > 50) index = dx < 0 ? Math.min(track.children.length - 1, index + 1) : Math.max(0, index - 1);
      updateCarouselPosition();
      startX = null;
    }, { passive:true });
  }

  function renderCards(cards, smooth = true) {
    if (!cards?.length) return;
    const hrefs = cards.map(c => c.href).join('|');
    if (hrefs === lastRenderedHrefs) { setSlideSizes(); updateCarouselPosition(); return; }
    lastRenderedHrefs = hrefs;

    const temp = document.createElement('div');
    temp.style.display = 'flex';
    temp.style.flexWrap = 'nowrap';

    for (const c of cards) {
      const a = document.createElement('a');
      a.className = 'nf-card';
      a.href = c.href;
      a.target = '_self';
      a.style.flex = '0 0 100%';
      a.innerHTML = `
        <div class="nf-card-img"><img src="${c.img}" alt="${escapeHtml(c.title)}"><div class="nf-card-badge">🔥 Горячая тема</div></div>
        <div class="nf-card-content">
          <div class="nf-card-title">${escapeHtml(c.title)}</div>
          <div class="nf-card-desc">${escapeHtml(c.text || '')}</div>
          <div class="nf-card-meta" style="display:flex;align-items:center;gap:8px;">
            <img src="${c.avatar}" style="width:48px;height:48px;border-radius:50%;">
            <span class="author-nick">${escapeHtml(c.author)}</span>
            <span class="views-count">👁️ ${c.views}</span>
          </div>
        </div>`;
      temp.appendChild(a);
    }

    const rect = track.getBoundingClientRect();
    const curH = rect.height || track.offsetHeight || 200;
    track.style.minHeight = curH + 'px';

    if (smooth) {
      track.style.transition = 'opacity 220ms ease';
      track.style.opacity = '0';
      setTimeout(() => {
        track.innerHTML = temp.innerHTML;
        setSlideSizes();
        if (index > track.children.length - 1) index = Math.max(0, track.children.length - 1);
        updateCarouselPosition();
        track.style.opacity = '1';
        setTimeout(() => { track.style.minHeight = ''; }, 300);
      }, 220);
    } else {
      track.innerHTML = temp.innerHTML;
      setSlideSizes();
      if (index > track.children.length - 1) index = Math.max(0, track.children.length - 1);
      updateCarouselPosition();
      track.style.minHeight = '';
    }

    bindArrowsOnce();
  }

  async function loadData(force=false) {
    const now = Date.now();
    if (inProgress || (!force && now - lastFetch < MIN_REFRESH_MS)) return;
    inProgress = true;
    lastFetch = now;
    try {
      let threads = findHotInDoc(document);
      if (!threads.length) {
        const html = await fetchHTML(BASE_FORUM_URL);
        const doc = new DOMParser().parseFromString(html, 'text/html');
        threads = findHotInDoc(doc);
      }
      threads = uniqueThreads(threads).slice(0, MAX_CARDS);
      if (!threads.length) { inProgress=false; return; }
      const cards = await Promise.all(threads.map(enrichThread));
      try { localStorage.setItem(CACHE_KEY, JSON.stringify({ time: Date.now(), cards })); } catch(e){}
      renderCards(cards, true);
    } catch(err) {
      console.error('nfCarousel loadData error', err);
    } finally {
      inProgress = false;
    }
  }

  try {
    const cached = JSON.parse(localStorage.getItem(CACHE_KEY) || '{}');
    if (cached.cards && Date.now() - (cached.time || 0) < CACHE_TIME) {
      renderCards(cached.cards, false);
    } else {
      await loadData(true);
    }
  } catch(e) {
    await loadData(true);
  }

  // Авто-обновление каждые 10 секунд
  setInterval(() => loadData(false), 10 * 1000);

  window.addEventListener('resize', () => { setSlideSizes(); updateCarouselPosition(); }, { passive: true });

})();
</script>


Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
Дата: Суббота, 25.10.2025, 14:05 | Сообщение # 2 | | Написал: Начинающий
Автор темы
Мурчанн не в сети
        Сообщений:101
         Регистрация:20.10.2016

Парсер со стилями. 12

Код
<style>
/* Неоновая сиреневая обёртка */
.nf-dark-border {
position: relative;
border: 1px solid #9D4EDD;
padding: 30px;
background-color: #0a0710;
color: #fff;
width: 850px;
margin: 40px auto;
border-radius: 12px;
box-shadow:
0 0 25px rgba(157, 78, 221, 0.7),
0 0 50px rgba(157, 78, 221, 0.5),
inset 0 0 20px rgba(157, 78, 221, 0.25);
animation: nf-neonGlow 3s ease-in-out infinite alternate;
overflow: visible;
}

/* Анимация сияния */
@keyframes nf-neonGlow {
from {
box-shadow:
0 0 15px rgba(157, 78, 221, 0.5),
0 0 30px rgba(157, 78, 221, 0.3),
inset 0 0 12px rgba(157, 78, 221, 0.15);
}
to {
box-shadow:
0 0 35px rgba(157, 78, 221, 0.9),
0 0 70px rgba(157, 78, 221, 0.7),
inset 0 0 25px rgba(157, 78, 221, 0.3);
}
}

/* Контейнер карусели */
.nf-carousel-container {
position: relative;
width: 100%;
overflow: hidden;
}

/* Трек карточек */
.nf-carousel-track {
display: flex;
transition: transform 0.5s ease;
}

/* Карточки */
.nf-card {
flex: 0 0 100%;
background: linear-gradient(145deg, #1c1122, #2a1735);
border-radius: 14px;
overflow: hidden;
display: flex;
text-decoration: none;
color: #fff;
cursor: pointer;
position: relative;
transition: box-shadow 0.3s ease, filter 0.3s ease;
min-height: 250px;
aspect-ratio: 16 / 9;
}

/* Карточка при наведении */
.nf-card:hover {
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
filter: brightness(1.45); /* мягче подсветка */
}

/* Изображение */
.nf-card-img {
width: 55%;
height: 100%;
overflow: hidden;
}

.nf-card-img img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}

.nf-card:hover .nf-card-img img {
transform: scale(1.03);
}

/* Контент карточки */
.nf-card-content {
padding: 25px 30px;
display: flex;
flex-direction: column;
justify-content: flex-start; /* заголовок сверху */
width: 45%;
}

.nf-card-title {
font-weight: bold;
font-size: 26px;
margin-bottom: 20px; /* немного больше пространства */
color: #e0b3ff;
text-shadow: 0 0 10px rgba(157, 78, 221, 0.6);
}

.nf-card-desc {
font-size: 16px;
color: #d8c3f0;
line-height: 1.7;
text-align: justify;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 12; /* длиннее описание */
-webkit-box-orient: vertical;
flex-grow: 1; /* описание заполняет пространство */
}

/* Автор */
.nf-card-meta {
position: absolute;
bottom: 15px;
right: 18px;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
z-index: 5;
}

.nf-card-meta img {
width: 55px;
height: 55px;
border-radius: 50%;
border: 2px solid #b367ff;
object-fit: cover;
background: #1a0e25;
box-shadow: 0 0 10px rgba(157, 78, 221, 0.6);
}

.nf-card-meta .author-nick {
font-size: 14px;
font-weight: bold;
color: #d7a7ff;
background: rgba(0, 0, 0, 0.45);
padding: 3px 10px;
border-radius: 8px;
backdrop-filter: blur(3px);
text-shadow: 0 0 6px rgba(157, 78, 221, 0.8);
}

/* Бейдж */
.nf-card-badge {
position: absolute;
top: 10px;
left: 10px;
background: linear-gradient(135deg, #b367ff, #7a3dbd);
padding: 5px 9px;
border-radius: 8px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.6px;
box-shadow: 0 0 12px rgba(157, 78, 221, 0.6);
}

/* Стрелки */
.nf-carousel-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 52px;
height: 52px;
background: radial-gradient(circle at center, rgba(157, 78, 221, 0.85), rgba(80, 30, 140, 0.8));
border-radius: 50%;
color: #fff;
font-size: 30px;
text-align: center;
line-height: 52px;
cursor: pointer;
user-select: none;
z-index: 20;
box-shadow: 0 0 20px rgba(157, 78, 221, 0.8), 0 0 35px rgba(157, 78, 221, 0.6);
transition: all 0.3s ease;
}

.nf-carousel-arrow:hover {
background: radial-gradient(circle at center, rgba(180, 100, 255, 0.9), rgba(100, 40, 160, 0.85));
transform: translateY(-50%) scale(1.1);
}

.nf-carousel-arrow:active {
background: rgba(157, 78, 221, 0.7);
}

.nf-carousel-arrow-left {
left: -25px;
}

.nf-carousel-arrow-right {
right: -25px;
}

/* Нижняя панель телевизора */
.nf-tv-bottom {
position: relative;
width: 100%;
height: 40px; /* высота панели */
background-color: #000; /* черная панель */
display: flex;
justify-content: center;
align-items: center;
font-family: 'Arial', sans-serif;
font-weight: bold;
font-size: 18px;
color: #fff;
letter-spacing: 2px;
text-shadow: 0 0 6px rgba(255,255,255,0.5);
border-top: 0px solid rgba(157,78,221,0.5); /* тонкая светящаяся линия сверху */
margin-top: 10px;
border-radius: 0 0 12px 12px; /* скругление как у рамки */
margin-top: 1px; /* или отрицательное значение, например -5px, чтобы поднять выше */
}

.tablet-stylus {
position: absolute;

/* Горизонтальное положение: left или right */
left: 280px; ; /* если хочешь слева */
right: 100px; /* если хочешь справа, регулируй значение */

/* Вертикальное положение */
top: 30%; /* центр по высоте */
transform: translateY(-50%) rotate(0deg) scale(1); /* центровка + угол + масштаб */

/* Размеры */
width: 14px; /* толщина стилуса */
height: 400px; /* длина стилуса */

/* Дизайн */
background: linear-gradient(180deg, #ffffff, #e6e6e6);
border-radius: 10px;
box-shadow:
inset 0 0 4px rgba(255,255,255,0.7),
0 0 8px rgba(255,255,255,0.8),
0 0 12px rgba(255,255,255,0.5);
z-index: 10;

/* Дополнительно для регулировки */
/* margin-left или margin-right можно использовать для точной подстройки */
}

/* Кончик стилуса */
.tablet-stylus::after {
content: '';
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
width: 8px;
height: 8px;
background: radial-gradient(circle at center, #ccc 20%, #999 80%);
border-radius: 50%;
box-shadow: 0 0 3px rgba(0,0,0,0.4);
}

</style>

<div class="nf-dark-border">
<div class="nf-carousel-container">

<div class="nf-carousel-track" id="nf-forum-cards"></div>

<!-- Нижняя панель телевизора -->
<div class="nf-tv-bottom">
<span>SAMSUNG</span>
</div>

</div>

<div class="nf-carousel-arrow nf-carousel-arrow-left">❮</div>
<div class="nf-carousel-arrow nf-carousel-arrow-right">❯</div>
</div>

<!-- Белый стилус сбоку планшета -->
<div class="tablet-stylus"></div>

<script>
(async function nfForumCarouselHot3_fixedCarousel() {
  const track = document.querySelector('.nf-carousel-track');
  if (!track) return;

  const arrowLeft = document.querySelector('.nf-carousel-arrow-left');
  const arrowRight = document.querySelector('.nf-carousel-arrow-right');
  const container = track.closest('.nf-carousel-container') || track.parentElement;

const BASE_FORUM_URL = '/forum/0-0-1-34';
const CACHE_KEY = 'nf_forum_hot_3_clean';
const CACHE_TIME = 30 * 60 * 1000;   // 30 минут кэш
const MIN_REFRESH_MS = 5000;         // минимальная задержка 5 секунд
const MAX_CARDS = 16;                // макс. количество карточек

  let lastFetch = 0;
  let inProgress = false;
  let lastRenderedHrefs = '';
  let index = 0;
  let arrowsBound = false;

  function toAbs(url) {
    if (!url) return '';
    if (url.startsWith('http')) return url;
    if (url.startsWith('//')) return window.location.protocol + url;
    return window.location.origin + (url.startsWith('/') ? url : '/' + url);
  }

  function escapeHtml(s) {
    return String(s || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
  }

  // --- Чистый текст без спецсимволов и больших пробелов ---
function getCleanText(el, maxLength = 220) {
    if (!el) return '';
    let text = '';
    const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false);
    let node;
    while(node = walker.nextNode()) {
        // пропускаем скрипты, стили и iframe
        if(node.parentNode.closest('script, style, iframe')) continue;
        let t = node.nodeValue;
        if(!t) continue;

        // убираем табы, переносы строк, неразрывные пробелы
        t = t.replace(/[\t\n\r\u00A0]+/g, ' ');

        // убираем лишние пробелы
        t = t.replace(/\s+/g, ' ').trim();

        if(t) text += t + ' ';
    }

    // пробелы перед знаками препинания
    text = text.replace(/\s+([.,!?;:])/g, '$1');

    // полностью удаляем HTML-теги, если что-то осталось
    text = text.replace(/<[^>]+>/g, '');

    // удаляем CSS свойства и подобное
    text = text.replace(/\b(?:margin|padding|width|height|text-align|border-radius|box-shadow|color|background|font|line-height)\s*:[^;]+;/gi, '');

    // убираем все странные спецсимволы, оставляем буквы, цифры, базовую пунктуацию
    text = text.replace(/[^\wа-яА-Я0-9.,!?;: \-()'"«»]/g, '');

    // обрезаем до maxLength символов
    return text.trim().slice(0, maxLength);
}

  async function fetchHTML(url) {
    const res = await fetch(url, { credentials: 'same-origin', cache: 'no-cache' });
    return await res.text();
  }

  function findHotInDoc(doc) {
    const rows = Array.from(doc.querySelectorAll('td.threadNametd, .threadNametd, tr'));
    const result = [];
    for (let td of rows) {
      const badge = td.querySelector('.ipsBadge');
      const pages = td.querySelector('.postpSwithces, .postPSwithcesLink');
      if (!((badge && /горяч/i.test(badge.textContent || '')) || pages)) continue;
      const linkEl = td.querySelector('a.threadLink');
      if (!linkEl) continue;
      const href = toAbs(linkEl.getAttribute('href') || linkEl.href || '');
      const title = (linkEl.textContent || '').trim() || '(без названия)';
      const row = td.closest('tr');
      let icon = row?.querySelector('img')?.getAttribute('src') || '';
      icon = toAbs(icon) || '/forumimages/Newsletter-3.png';
      const author = (row?.querySelector('.threadAuthTd')?.textContent || '').trim() || 'Аноним';
      const views = parseInt((row?.querySelector('.threadViewTd, .views')?.textContent || '').replace(/\D/g, '')) || 0;
      result.push({ href, title, icon, author, views });
      if (result.length >= MAX_CARDS) break;
    }
    return result;
  }

  function uniqueThreads(arr) {
    const seen = new Set();
    return arr.filter(t => {
      if (seen.has(t.href)) return false;
      seen.add(t.href);
      return true;
    });
  }

  async function enrichThread(t) {
    try {
      const txt = await fetchHTML(t.href);
      const doc = new DOMParser().parseFromString(txt, 'text/html');
      const postEl = doc.querySelector('.post_content, .post_body, .post, .message, .ipsType_richText');
      const text = getCleanText(postEl);
      let img = '';
      if (postEl) {
        for (let im of Array.from(postEl.querySelectorAll('img'))) {
          const src = im.getAttribute('data-src') || im.src || '';
          if (!src) continue;
          if (/avatar|forumimages|template/i.test(src)) continue;
          img = toAbs(src); break;
        }
      }
      if (!img) img = t.icon;
      const authorEl = doc.querySelector('.postUser, .author, .post-author-name');
      const author = (authorEl?.textContent?.trim()) || t.author || 'Аноним';
      const avatarEl = doc.querySelector('.avatar img, .ipsUserPhoto');
      const avatar = toAbs(avatarEl?.src || '/forumimages/default-avatar.png');
      return { ...t, text, img, author, avatar };
    } catch (e) {
      return { ...t, text: '', img: t.icon, avatar: '/forumimages/default-avatar.png' };
    }
  }

  function setSlideSizes() {
    const slides = Array.from(track.children);
    const width = (container?.clientWidth || track.clientWidth || 600);
    slides.forEach(s => s.style.width = width + 'px');
    track.style.display = 'flex';
    track.style.transition = 'transform 0.45s ease';
  }

  function updateCarouselPosition() {
    const total = track.children.length || 1;
    if (index < 0) index = 0;
    if (index > total - 1) index = Math.max(0, total - 1);
    const width = (container?.clientWidth || track.clientWidth || 600);
    track.style.transform = `translateX(${-index * width}px)`;
    if (arrowLeft) arrowLeft.style.opacity = index === 0 ? '0.4' : '1';
    if (arrowRight) arrowRight.style.opacity = index >= total - 1 ? '0.4' : '1';
  }

  function bindArrowsOnce() {
    if (arrowsBound) return;
    arrowsBound = true;
    arrowLeft?.addEventListener('click', () => { index = Math.max(0, index - 1); updateCarouselPosition(); });
    arrowRight?.addEventListener('click', () => { index = Math.min(track.children.length - 1, index + 1); updateCarouselPosition(); });
    window.addEventListener('keydown', e => {
      if (e.key === 'ArrowLeft') index = Math.max(0, index - 1), updateCarouselPosition();
      if (e.key === 'ArrowRight') index = Math.min(track.children.length - 1, index + 1), updateCarouselPosition();
    });
    let startX = null;
    track.addEventListener('touchstart', e => startX = e.touches[0].clientX, { passive:true });
    track.addEventListener('touchend', e => {
      if (startX === null) return;
      const dx = e.changedTouches[0].clientX - startX;
      if (Math.abs(dx) > 50) index = dx < 0 ? Math.min(track.children.length - 1, index + 1) : Math.max(0, index - 1);
      updateCarouselPosition();
      startX = null;
    }, { passive:true });
  }

  function renderCards(cards, smooth = true) {
    if (!cards?.length) return;
    const hrefs = cards.map(c => c.href).join('|');
    if (hrefs === lastRenderedHrefs) { setSlideSizes(); updateCarouselPosition(); return; }
    lastRenderedHrefs = hrefs;

    const temp = document.createElement('div');
    temp.style.display = 'flex';
    temp.style.flexWrap = 'nowrap';

    for (const c of cards) {
      const a = document.createElement('a');
      a.className = 'nf-card';
      a.href = c.href;
      a.target = '_self';
      a.style.flex = '0 0 100%';
      a.innerHTML = `
        <div class="nf-card-img"><img src="${c.img}" alt="${escapeHtml(c.title)}"><div class="nf-card-badge">🔥 Горячая тема</div></div>
        <div class="nf-card-content">
          <div class="nf-card-title">${escapeHtml(c.title)}</div>
          <div class="nf-card-desc">${escapeHtml(c.text || '')}</div>
          <div class="nf-card-meta" style="display:flex;align-items:center;gap:8px;">
            <img src="${c.avatar}" style="width:48px;height:48px;border-radius:50%;">
            <span class="author-nick">${escapeHtml(c.author)}</span>
            <span class="views-count">👁️ ${c.views}</span>
          </div>
        </div>`;
      temp.appendChild(a);
    }

    const rect = track.getBoundingClientRect();
    const curH = rect.height || track.offsetHeight || 200;
    track.style.minHeight = curH + 'px';

    if (smooth) {
      track.style.transition = 'opacity 220ms ease';
      track.style.opacity = '0';
      setTimeout(() => {
        track.innerHTML = temp.innerHTML;
        setSlideSizes();
        if (index > track.children.length - 1) index = Math.max(0, track.children.length - 1);
        updateCarouselPosition();
        track.style.opacity = '1';
        setTimeout(() => { track.style.minHeight = ''; }, 300);
      }, 220);
    } else {
      track.innerHTML = temp.innerHTML;
      setSlideSizes();
      if (index > track.children.length - 1) index = Math.max(0, track.children.length - 1);
      updateCarouselPosition();
      track.style.minHeight = '';
    }

    bindArrowsOnce();
  }

  async function loadData(force=false) {
    const now = Date.now();
    if (inProgress || (!force && now - lastFetch < MIN_REFRESH_MS)) return;
    inProgress = true;
    lastFetch = now;
    try {
      let threads = findHotInDoc(document);
      if (!threads.length) {
        const html = await fetchHTML(BASE_FORUM_URL);
        const doc = new DOMParser().parseFromString(html, 'text/html');
        threads = findHotInDoc(doc);
      }
      threads = uniqueThreads(threads).slice(0, MAX_CARDS);
      if (!threads.length) { inProgress=false; return; }
      const cards = await Promise.all(threads.map(enrichThread));
      try { localStorage.setItem(CACHE_KEY, JSON.stringify({ time: Date.now(), cards })); } catch(e){}
      renderCards(cards, true);
    } catch(err) {
      console.error('nfCarousel loadData error', err);
    } finally {
      inProgress = false;
    }
  }

  try {
    const cached = JSON.parse(localStorage.getItem(CACHE_KEY) || '{}');
    if (cached.cards && Date.now() - (cached.time || 0) < CACHE_TIME) {
      renderCards(cached.cards, false);
    } else {
      await loadData(true);
    }
  } catch(e) {
    await loadData(true);
  }

  // Авто-обновление каждые 10 секунд
  setInterval(() => loadData(false), 10 * 1000);

  window.addEventListener('resize', () => { setSlideSizes(); updateCarouselPosition(); }, { passive: true });

})();
</script>

Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
Дата: Суббота, 25.10.2025, 20:43 | Сообщение # 3 | | Написал: Новичок
Massimo не в сети
        Сообщений:21
         Регистрация:10.09.2014

Выглядит презентабельно , только вот Стилиус всегда теряется где - то , меняет положение независимо от планшета. 2

Massimo

Не стоить «склеивать разбитые горшки» — нужно просто жить, жить настоящим, хотя оно никогда не бывает таким, каким мы его задумали.
  • Страница 1 из 1
  • 1
Поиск: