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

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

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

Это первая версия парсера. Логика скрипта крайне проста: он просматривает ленту последних сообщений форума и выбирает темы, находящиеся в категории «горячие», чтобы вывести их на главную страницу.

Парсер извлекает следующие данные:

Аватар автора темы

Количество сообщений

Заголовок темы

Картинку темы

В настройках установлено отображение только 6 последних горячих тем.

От идеи автоматического слайдера с прокруткой я отказался. Вместо этого реализованы ручные стрелки для перелистывания, а также эффекты при наведении: обычно карточки затемняются, но в моем варианте яркость увеличивается, чтобы пользователю было удобнее читать тему.

Парсер полностью модифицируемый: можно вносить изменения или добавлять новые функции. Например, можно вытягивать дату и время создания темы, рейтинг, комментарии и практически любые другие данные.

Обычное состояние на главной.



При наведении на блок горячих тем.



Создаем глобальный блок на сайте для удобства и вставляем куда надо.

Код
<!-- <body> -->
<section class="sect flex-grow-1">
<div class="sect__header d-flex">
<h2 class="sect__title flex-grow-1">Горячая тема</h2>

<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;
}

</style>

<div class="nf-dark-border">
<div class="nf-carousel-container">
<div class="nf-carousel-track" id="nf-forum-cards"></div>
</div>
<div class="nf-carousel-arrow nf-carousel-arrow-left">❮</div>
<div class="nf-carousel-arrow nf-carousel-arrow-right">❯</div>
</div>

<script>
(async function nfForumCarouselHot3_carousel() {
  const track = document.querySelector('.nf-carousel-track');
  const fallbackContainer = document.querySelector('#nf-forum-cards');
  const cardsHost = track || fallbackContainer;
  if (!cardsHost) return;

  const arrowLeft = document.querySelector('.nf-carousel-arrow-left');
  const arrowRight = document.querySelector('.nf-carousel-arrow-right');

  const BASE_FORUM_URL = '/forum/0-0-1-34';
  const CACHE_KEY = 'nf_forum_hot_3_clean';
  const CACHE_TIME = 30 * 60 * 1000;
  const MAX_CARDS = 6;

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

  const extra = Array.from(document.querySelectorAll('#nf-forum-cards')).slice(1);
  extra.forEach(e => e.remove());

  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 titleEl = linkEl.querySelector('.ipsType_pagetitle') || linkEl;
      const title = (titleEl && titleEl.textContent || '').trim() || '(без названия)';

      const row = td.closest('tr');
      let icon = row ? (row.querySelector('.threadIcoTd img') || row.querySelector('td img')) : null;
      icon = icon ? icon.getAttribute('src') || icon.src : '';
      icon = toAbs(icon) || '/forumimages/Newsletter-3.png';

      let author = '';
      const authTd = row ? row.querySelector('.threadAuthTd') : null;
      if (authTd) author = authTd.textContent.trim();
      if (!author) {
        const authorLink = td.querySelector('a[href*="/index/"], .lastPostUser, .postUser, .author');
        if (authorLink) author = authorLink.textContent.trim();
      }
      if (!author) author = 'Аноним';

      const viewsEl = row ? row.querySelector('.threadViewTd, .views, .thread-views') : null;
      let views = 0;
      if (viewsEl) {
        const m = (viewsEl.textContent||'').replace(/\s/g,'').match(/(\d{1,})/);
        if (m) views = parseInt(m[1],10);
      }

      result.push({ href, title, author, icon, views });
    }

    const unique = [];
    const seen = new Set();
    for (let t of result) {
      if (seen.has(t.href)) continue;
      seen.add(t.href);
      unique.push(t);
      if (unique.length >= MAX_CARDS) break;
    }
    return unique;
  }

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

  async function enrichThread(t) {
    try {
      const txt = await fetchHTML(t.href);
      const parser = new DOMParser();
      const doc = parser.parseFromString(txt, 'text/html');

      const postEl = doc.querySelector('.post_content, .post_body, .post, .ipsType_richText, .message');
      const text = postEl ? (postEl.textContent || '').trim().replace(/\s+/g,' ').slice(0,200) : '';

      let img = '';
      if (postEl) {
        const imgs = Array.from(postEl.querySelectorAll('img'));
        for (let im of imgs) {
          const src = im.getAttribute('ilo-full-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;

      let author = t.author;
      const firstAuthor = doc.querySelector('.postUser, .author, .post-author-name');
      if (firstAuthor && firstAuthor.textContent.trim()) author = firstAuthor.textContent.trim();

      const avatarEl = doc.querySelector('.ipsUserPhoto, .avatar, .user-avatar img');
      let avatar = avatarEl ? (avatarEl.src || '') : '/forumimages/default-avatar.png';
      if (avatar && !avatar.startsWith('http')) avatar = window.location.origin + (avatar.startsWith('/') ? avatar : '/' + avatar);

      return { ...t, text, img, author, avatar };
    } catch (e) {
      return { ...t, text: '', img: t.icon, avatar: '/forumimages/default-avatar.png' };
    }
  }

  try {
    const cached = localStorage.getItem(CACHE_KEY);
    if (cached) {
      const data = JSON.parse(cached);
      if (Date.now() - data.time < CACHE_TIME) {
        renderCarousel(data.cards);
        return;
      }
    }
  } catch(e){}

  let threads = findHotInDoc(document);
  if (!threads.length) {
    try {
      const html = await fetchHTML(BASE_FORUM_URL);
      const parser = new DOMParser();
      const doc = parser.parseFromString(html, 'text/html');
      threads = findHotInDoc(doc);
    } catch(e){ }
  }

  if (!threads.length) {
    cardsHost.innerHTML = '<div style="color:#fff; padding:12px;">🔥 Нет горячих тем</div>';
    return;
  }

  const cards = await Promise.all(threads.slice(0, MAX_CARDS).map(enrichThread));
  try { localStorage.setItem(CACHE_KEY, JSON.stringify({ time: Date.now(), cards })); } catch(e){}

  renderCarousel(cards);

  function renderCarousel(cards) {
    cardsHost.innerHTML = '';
    const hostIsTrack = !!track;
    const trackEl = hostIsTrack ? cardsHost : document.createElement('div');
    if (!hostIsTrack) {
      trackEl.className = 'nf-carousel-track';
      cardsHost.appendChild(trackEl);
    }

    cards.forEach(c => {
      const a = document.createElement('a');
      a.className = 'nf-card';
      a.href = c.href;
      a.target = '_self';
      a.style.flex = '0 0 100%';
      a.style.boxSizing = 'border-box';

      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}" alt="${escapeHtml(c.author)}" 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>
      `;
      trackEl.appendChild(a);
    });

    if (!hostIsTrack) {
      const wrap = document.querySelector('.nf-carousel-container') || cardsHost;
      wrap.innerHTML = '';
      wrap.appendChild(trackEl);
    }

    let index = 0;
    const slides = Array.from(trackEl.children);
    const total = slides.length;
    const visibleWrap = trackEl.closest('.nf-carousel-container') || trackEl.parentElement;

    function update() {
      const width = visibleWrap.clientWidth || visibleWrap.getBoundingClientRect().width;
      const x = -index * width;
      trackEl.style.transform = `translateX(${x}px)`;
      if (arrowLeft) arrowLeft.style.opacity = index === 0 ? '0.4' : '1';
      if (arrowRight) arrowRight.style.opacity = index >= total - 1 ? '0.4' : '1';
    }

    function setSlideSizes() {
      const width = visibleWrap.clientWidth || visibleWrap.getBoundingClientRect().width;
      slides.forEach(s => s.style.width = width + 'px');
      trackEl.style.display = 'flex';
      trackEl.style.transition = 'transform 0.45s ease';
      update();
    }

    window.addEventListener('resize', setSlideSizes);
    setSlideSizes();

    if (arrowLeft) arrowLeft.onclick = () => { index = Math.max(0, index - 1); update(); };
    if (arrowRight) arrowRight.onclick = () => { index = Math.min(total - 1, index + 1); update(); };

    let startX = null;
    trackEl.addEventListener('touchstart', e => { startX = e.touches[0].clientX; }, {passive:true});
    trackEl.addEventListener('touchend', e => {
      if (startX === null) return;
      const endX = (e.changedTouches && e.changedTouches[0].clientX) || 0;
      const dx = endX - startX;
      if (Math.abs(dx) > 50) {
        index = dx < 0 ? Math.min(total - 1, index + 1) : Math.max(0, index - 1);
        update();
      }
      startX = null;
    }, {passive:true});

    window.addEventListener('keydown', e => {
      if (e.key === 'ArrowLeft') { index = Math.max(0, index - 1); update(); }
      if (e.key === 'ArrowRight') { index = Math.min(total - 1, index + 1); update(); }
    });
  }

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

</div>


Несмотря на свою простоту, скрипт демонстрирует элегантное и продуманное решение задачи: он минималистичен, легко расширяем и при этом максимально эффективен в работе. 12

Мурчанн

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

По отдельности

CSS

Код
<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;
}

</style>


Скрипт

Код
<script>
(async function nfForumCarouselHot3_carousel() {
  const track = document.querySelector('.nf-carousel-track');
  const fallbackContainer = document.querySelector('#nf-forum-cards');
  const cardsHost = track || fallbackContainer;
  if (!cardsHost) return;

  const arrowLeft = document.querySelector('.nf-carousel-arrow-left');
  const arrowRight = document.querySelector('.nf-carousel-arrow-right');

  const BASE_FORUM_URL = '/forum/0-0-1-34';
  const CACHE_KEY = 'nf_forum_hot_3_clean';
  const CACHE_TIME = 30 * 60 * 1000;
  const MAX_CARDS = 6;

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

  const extra = Array.from(document.querySelectorAll('#nf-forum-cards')).slice(1);
  extra.forEach(e => e.remove());

  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 titleEl = linkEl.querySelector('.ipsType_pagetitle') || linkEl;
      const title = (titleEl && titleEl.textContent || '').trim() || '(без названия)';

      const row = td.closest('tr');
      let icon = row ? (row.querySelector('.threadIcoTd img') || row.querySelector('td img')) : null;
      icon = icon ? icon.getAttribute('src') || icon.src : '';
      icon = toAbs(icon) || '/forumimages/Newsletter-3.png';

      let author = '';
      const authTd = row ? row.querySelector('.threadAuthTd') : null;
      if (authTd) author = authTd.textContent.trim();
      if (!author) {
        const authorLink = td.querySelector('a[href*="/index/"], .lastPostUser, .postUser, .author');
        if (authorLink) author = authorLink.textContent.trim();
      }
      if (!author) author = 'Аноним';

      const viewsEl = row ? row.querySelector('.threadViewTd, .views, .thread-views') : null;
      let views = 0;
      if (viewsEl) {
        const m = (viewsEl.textContent||'').replace(/\s/g,'').match(/(\d{1,})/);
        if (m) views = parseInt(m[1],10);
      }

      result.push({ href, title, author, icon, views });
    }

    const unique = [];
    const seen = new Set();
    for (let t of result) {
      if (seen.has(t.href)) continue;
      seen.add(t.href);
      unique.push(t);
      if (unique.length >= MAX_CARDS) break;
    }
    return unique;
  }

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

  async function enrichThread(t) {
    try {
      const txt = await fetchHTML(t.href);
      const parser = new DOMParser();
      const doc = parser.parseFromString(txt, 'text/html');

      const postEl = doc.querySelector('.post_content, .post_body, .post, .ipsType_richText, .message');
      const text = postEl ? (postEl.textContent || '').trim().replace(/\s+/g,' ').slice(0,200) : '';

      let img = '';
      if (postEl) {
        const imgs = Array.from(postEl.querySelectorAll('img'));
        for (let im of imgs) {
          const src = im.getAttribute('ilo-full-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;

      let author = t.author;
      const firstAuthor = doc.querySelector('.postUser, .author, .post-author-name');
      if (firstAuthor && firstAuthor.textContent.trim()) author = firstAuthor.textContent.trim();

      const avatarEl = doc.querySelector('.ipsUserPhoto, .avatar, .user-avatar img');
      let avatar = avatarEl ? (avatarEl.src || '') : '/forumimages/default-avatar.png';
      if (avatar && !avatar.startsWith('http')) avatar = window.location.origin + (avatar.startsWith('/') ? avatar : '/' + avatar);

      return { ...t, text, img, author, avatar };
    } catch (e) {
      return { ...t, text: '', img: t.icon, avatar: '/forumimages/default-avatar.png' };
    }
  }

  try {
    const cached = localStorage.getItem(CACHE_KEY);
    if (cached) {
      const data = JSON.parse(cached);
      if (Date.now() - data.time < CACHE_TIME) {
        renderCarousel(data.cards);
        return;
      }
    }
  } catch(e){}

  let threads = findHotInDoc(document);
  if (!threads.length) {
    try {
      const html = await fetchHTML(BASE_FORUM_URL);
      const parser = new DOMParser();
      const doc = parser.parseFromString(html, 'text/html');
      threads = findHotInDoc(doc);
    } catch(e){ }
  }

  if (!threads.length) {
    cardsHost.innerHTML = '<div style="color:#fff; padding:12px;">🔥 Нет горячих тем</div>';
    return;
  }

  const cards = await Promise.all(threads.slice(0, MAX_CARDS).map(enrichThread));
  try { localStorage.setItem(CACHE_KEY, JSON.stringify({ time: Date.now(), cards })); } catch(e){}

  renderCarousel(cards);

  function renderCarousel(cards) {
    cardsHost.innerHTML = '';
    const hostIsTrack = !!track;
    const trackEl = hostIsTrack ? cardsHost : document.createElement('div');
    if (!hostIsTrack) {
      trackEl.className = 'nf-carousel-track';
      cardsHost.appendChild(trackEl);
    }

    cards.forEach(c => {
      const a = document.createElement('a');
      a.className = 'nf-card';
      a.href = c.href;
      a.target = '_self';
      a.style.flex = '0 0 100%';
      a.style.boxSizing = 'border-box';

      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}" alt="${escapeHtml(c.author)}" 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>
      `;
      trackEl.appendChild(a);
    });

    if (!hostIsTrack) {
      const wrap = document.querySelector('.nf-carousel-container') || cardsHost;
      wrap.innerHTML = '';
      wrap.appendChild(trackEl);
    }

    let index = 0;
    const slides = Array.from(trackEl.children);
    const total = slides.length;
    const visibleWrap = trackEl.closest('.nf-carousel-container') || trackEl.parentElement;

    function update() {
      const width = visibleWrap.clientWidth || visibleWrap.getBoundingClientRect().width;
      const x = -index * width;
      trackEl.style.transform = `translateX(${x}px)`;
      if (arrowLeft) arrowLeft.style.opacity = index === 0 ? '0.4' : '1';
      if (arrowRight) arrowRight.style.opacity = index >= total - 1 ? '0.4' : '1';
    }

    function setSlideSizes() {
      const width = visibleWrap.clientWidth || visibleWrap.getBoundingClientRect().width;
      slides.forEach(s => s.style.width = width + 'px');
      trackEl.style.display = 'flex';
      trackEl.style.transition = 'transform 0.45s ease';
      update();
    }

    window.addEventListener('resize', setSlideSizes);
    setSlideSizes();

    if (arrowLeft) arrowLeft.onclick = () => { index = Math.max(0, index - 1); update(); };
    if (arrowRight) arrowRight.onclick = () => { index = Math.min(total - 1, index + 1); update(); };

    let startX = null;
    trackEl.addEventListener('touchstart', e => { startX = e.touches[0].clientX; }, {passive:true});
    trackEl.addEventListener('touchend', e => {
      if (startX === null) return;
      const endX = (e.changedTouches && e.changedTouches[0].clientX) || 0;
      const dx = endX - startX;
      if (Math.abs(dx) > 50) {
        index = dx < 0 ? Math.min(total - 1, index + 1) : Math.max(0, index - 1);
        update();
      }
      startX = null;
    }, {passive:true});

    window.addEventListener('keydown', e => {
      if (e.key === 'ArrowLeft') { index = Math.max(0, index - 1); update(); }
      if (e.key === 'ArrowRight') { index = Math.min(total - 1, index + 1); update(); }
    });
  }

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

Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
  • Страница 1 из 1
  • 1
Поиск: