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

1
Админ
Постов: 211
2
VIP
Постов: 72
3
Элита
Постов: 50
4
Проверенные
Постов: 35
5
VIP
Постов: 35
6
Проверенные
Постов: 32
7
Пользователи
Постов: 31
8
Проверенные
Постов: 29

  • Страница 1 из 1
  • 1
Парсер ленты форума на главную страницу для uCoz V 6.0
Дата: Суббота, 21.02.2026, 18:15 | Сообщение # 1 | | Написал: Узнаваемый
Автор темы
Мурчанн не в сети
        Сообщений:211
         Регистрация:20.10.2016

Вживую его можно увидеть на главной странице. Это оригинальный парсер. Давно хотел создать подобный вариант именно в плане дизайна. Логика скрипта не менялась, используется всё тот же самый код, никаких изменений не вносилось. Работает исправно, багов не обнаружено.

Создаём глобальный блок «Лента» и вставляем в него парсер.



Код
<section class="sect flex-grow-1">
<div class="sect__header d-flex">
  <h2 class="sect__title flex-grow-1">
    Forum Updates
    <img src="https://jordan.moy.su/forumimages/3droom.png" alt="3D Room Icon" style="height:24px; margin-left:-2px; vertical-align:middle;">
  </h2>
</div>
<div class="dark-border">
  <div id="nf28_cards"></div>
</div>
</section>

<style>
/* ──────────────── CONTAINER ──────────────── */
#nf28_cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));   /* ← было 360 → теперь помещается больше карточек */
  gap: 30px 30px;                    /* чуть плотнее */
  width: 100%;
  padding: 0 12px;
  margin: 0 auto;
  max-width: 1600px;                    /* чуть шире, если экран позволяет */
  box-sizing: border-box;
}

/* ──────────────── CARD ──────────────── */
.nf28_card {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: #fff;
  border-radius: 16px;           /* чуть мягче углы */
  overflow: hidden;
  background: rgba(20, 20, 35, 0.42);
  backdrop-filter: blur(16px) saturate(180%);
  -webkit-backdrop-filter: blur(16px) saturate(180%);
  box-shadow: 0 10px 32px rgba(0,0,0,0.38),
              inset 0 1px 0 rgba(255,255,255,0.07);
  border: 1px solid rgba(255,255,255,0.05);
  transition: transform 0.32s cubic-bezier(0.34, 1.4, 0.64, 1),
              box-shadow 0.32s ease,
              opacity 0.32s ease;
  opacity: 0.97;
}

.nf28_card:hover,
.nf28_card:focus-visible {
  transform: translateY(-8px) scale(1.015);
  box-shadow: 0 20px 50px rgba(0,0,0,0.48),
              inset 0 1px 0 rgba(255,255,255,0.1);
  opacity: 1;
}

/* Image wrapper */
.nf28_img_wrapper {
  position: relative;
  width: 100%;
  height: 240px;                 /* ← было 260 → чуть ниже */
  overflow: hidden;
}

.nf28_img_wrapper img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
}

.nf28_card:hover .nf28_img_wrapper img {
  transform: scale(1.07);
}

/* Author badge */
.nf28_author {
  position: absolute;
  top: 12px;
  left: 12px;
  z-index: 3;
  font-size: 0.86rem;
  font-weight: 700;
  color: #fff;
  background: rgba(0,0,0,0.45);
  backdrop-filter: blur(8px);
  padding: 4px 10px;
  border-radius: 50px;
  border: 1px solid rgba(255,255,220,0.35);
  text-shadow: 0 1px 3px rgba(0,0,0,0.7);
}

.nf28_img_wrapper {
  position: relative;
  width: 100%;
  height: 240px;               /* высота картинки не меняем */
  overflow: hidden;
}

.nf28_title {
  position: absolute;
  bottom: 0px;                /* ← поднимаем заголовок выше  */
  left: 0;
  right: 0;
  padding: 16px 16px 24px;     /* больше padding снизу → градиент будет длиннее */
  font-size: clamp(1.1rem, 2.8vw, 1.40rem);
  font-weight: 700;
  line-height: 1.28;
  
  /* Делаем градиент гораздо выше, чтобы он закрывал пустоту под заголовком */
  background: linear-gradient(
    to top,
    rgba(0,0,0,0.92) 0%,
    rgba(0,0,0,0.85) 30%,
    rgba(0,0,0,0.6)  60%,
    transparent 100%
  );
  
  text-shadow: 0 2px 8px rgba(0,0,0,0.85);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  
  /* Важно: z-index выше картинки (на всякий случай) */
  z-index: 2;
}

/* Description block */
.nf28_desc_wrapper {
  padding: 16px;                 /* ← чуть компактнее */
  background: linear-gradient(to bottom, rgba(15,15,25,0.72) 0%, rgba(10,10,20,0.88) 100%);
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.nf28_desc {
  font-size: 1rem;               /* ← было 1.05 → чуть меньше */
  line-height: 1.55;
  color: #e0e0f0;
  opacity: 0.9;
  display: -webkit-box;
  -webkit-line-clamp: 4;         /* ← было 5 → компактнее */
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin: 0;
}

/* Footer */
.nf28_footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: auto;
  padding-top: 12px;
  border-top: 1px solid rgba(255,255,255,0.06);
  font-size: 0.87rem;
  color: #c0c0d8;
}

.nf28_date {
  font-weight: 600;
  background: linear-gradient(135deg, #0fd700, #7e1280, #f714d9);
  color: #fff;
  padding: 3px 7px;
  border-radius: 4px;
  font-size: 9.5px;
  text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
  box-shadow: 0 1px 5px rgba(0,0,0,0.2);
}

.nf28_button {
  background: linear-gradient(135deg, #8a5acf 0%, #c06ab8 100%);
  color: white;
  padding: 8px 18px;
  border-radius: 50px;
  font-weight: 700;
  font-size: 0.9rem;
  text-decoration: none;
  transition: all 0.3s ease;
  box-shadow: 0 3px 16px rgba(133,91,151,0.28);
  white-space: nowrap;
}

.nf28_button:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 28px rgba(133,91,151,0.42);
  background: linear-gradient(135deg, #9b6ee0 0%, #d47cc9 100%);
}

/* Responsive */
@media (max-width: 1100px) {
  #nf28_cards {
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: 20px 16px;
  }
}

@media (max-width: 800px) {
  #nf28_cards {
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  }
  .nf28_img_wrapper { height: 200px; }
  .nf28_title {
    font-size: clamp(1.2rem, 4vw, 1.4rem);
    padding: 14px 14px 12px;
  }
  .nf28_desc {
    font-size: 0.95rem;
    -webkit-line-clamp: 3;
  }
}

@media (max-width: 500px) {
  .nf28_img_wrapper { height: 180px; }
}

/* Fade-in animation */
@keyframes nf28FadeIn {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: translateY(0); }
}

.nf28_card {
  animation: nf28FadeIn 0.75s ease-out forwards;
  animation-delay: calc(var(--order, 0) * 100ms);
}

/* Градиентный оверлей — закрывает нижнюю часть картинки */
.nf28-gradient-overlay {
  position: absolute;
  inset: 0;                           /* заполняет весь .nf28_img_wrapper */
  background: linear-gradient(
    to top,
    rgba(0,0,0,0.92) 0%,                /* почти чёрный внизу */
    rgba(0,0,0,0.75) 30%,
    rgba(0,0,0,0.35) 60%,
    transparent 100%                    /* полностью прозрачный наверху */
  );
  pointer-events: none;               /* не мешает кликам */
  z-index: 1;                         /* выше картинки, но ниже текста */
}

/* Теперь заголовок можно поднимать сколько угодно */
.nf28_title {
  position: absolute;
  bottom: 15px;                       /* ← здесь  подъёма (40–90 px обычно хорошо) */
  left: 0;
  right: 0;
  padding: 16px 16px 20px;
  font-size: clamp(1.1rem, 2.8vw, 1.40rem);
  font-weight: 700;
  line-height: 1.28;
  background: none;                   /* фон больше не нужен его делает оверлей */
  color: #ffffff;
  text-shadow: 0 2px 10px rgba(0,0,0,0.9);
  z-index: 2;                         /* выше оверлея */
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.nf28-gradient-overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(0,0,0,0.78) 0%, rgba(0,0,0,0.55) 35%, transparent 80%);
  pointer-events: none;
  z-index: 1;
}

.nf28_title {
  bottom: 1px;
  padding: 16px 16px 28px;
  background: none;
  color: #ffffff;
  text-shadow: 0 2px 12px rgba(0,0,0,1);
  z-index: 2;
  /* остальные свойства как были */
}

</style>

<script>
(async function nf28ForumParser() {
  /* SINGLE INSTANCE LOCK */
  if (window.__NF28_PARSER_LOCK__) return;
  window.__NF28_PARSER_LOCK__ = true;

  /* ──────────────── CONFIG ──────────────── */
  const CONTAINER_ID = '#nf28_cards';
  const container = document.querySelector(CONTAINER_ID);
  if (!container) return;

  const FORUM_URL     = '/forum/0-0-1-34';
  const CACHE_KEY     = 'nf28_cards_cache_v3';
  const FIRST_KEY     = 'nf28_first_topic_v3';
  const MAX_CARDS     = 12;

  /* ──────────────── UTILS ──────────────── */
  const toAbs = url => !url ? '' :
    url.startsWith('http')  ? url :
    url.startsWith('//')    ? location.protocol + url :
    location.origin + (url.startsWith('/') ? url : '/' + url);

  const esc = text => String(text || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"','\'':'\''}[m]));

  const safeGet = key => { try { return JSON.parse(localStorage.getItem(key) || 'null') } catch { return null } };
  const safeSet = (key, val) => { try { localStorage.setItem(key, JSON.stringify(val)) } catch {} };
  const clearCache = () => localStorage.removeItem(CACHE_KEY);

  async function fetchHTML(url) {
    const r = await fetch(url, { credentials: 'same-origin', cache: 'no-cache' });
    if (!r.ok) throw new Error(`fetch failed: ${r.status}`);
    return await r.text();
  }

  /* ──────────────── CARD CREATION ──────────────── */
function createCard({ href, img, title, text, author, date }) {
  const card = document.createElement('a');
  card.className = 'nf28_card';
  card.href = href;
  card.target = '_self';

  card.innerHTML = `
    <div class="nf28_img_wrapper">
      <img src="${img || '/forumimages/Newsletter-3.png'}" alt="">
      
      <!-- Новый слой: градиент, который закрывает нижнюю часть картинки -->
      <div class="nf28-gradient-overlay"></div>
      
      <div class="nf28_author">${esc(author || 'Автор')}</div>
      <div class="nf28_title">${esc(title || 'Без названия')}</div>
    </div>
    <div class="nf28_desc_wrapper">
      <div class="nf28_desc">${esc(text || '').slice(0, 320)}</div>
      <div class="nf28_footer">
        <span class="nf28_date">${esc(date || 'Дата')}</span>
        <a href="${href}" class="nf28_button">Далее</a>
      </div>
    </div>`;

  return card;
}

  function render(cards) {
    if (!cards?.length) return;
    container.innerHTML = '';
    cards.forEach((c, i) => {
      const el = createCard(c);
      el.style.setProperty('--order', i);
      container.appendChild(el);
    });
  }

  /* ──────────────── PARSERS ──────────────── */
  function parseThreadList(doc) {
    const items = doc.querySelectorAll('.threadNametd .threadLink');
    return Array.from(items)
      .slice(0, MAX_CARDS)
      .map(a => ({
        href: toAbs(a.getAttribute('href') || ''),
        title: a.textContent.trim()
      }));
  }

  function parseFirstPost(html) {
    const doc = new DOMParser().parseFromString(html, 'text/html');
    let text = '', img = '', author = 'Автор', date = 'Дата';

    const post = doc.querySelector('.post_content, .post_body, .post, .ipsType_richText');
    if (post) {
      text = post.textContent.trim();
      const im = post.querySelector('img[src]');
      if (im) img = toAbs(im.getAttribute('src'));
    }

    const authorEl = doc.querySelector('.postUser, .author, [class*="author"], [class*="user"]');
    if (authorEl) author = authorEl.textContent.trim();

    // попытка найти дату (может потребовать адаптации под твой форум)
    const dateTd = [...doc.querySelectorAll('td, div, span')]
      .find(el => /Дата:|Опубликовано:|Добавлено:/i.test(el.textContent));
    if (dateTd) {
      const m = dateTd.textContent.match(/(?:Дата|Опубликовано|Добавлено):\s*([^|\n<]+)/i);
      if (m) date = m[1].trim();
    }

    return { text, img, author, date };
  }

  /* ──────────────── LOGIC ──────────────── */
  try {
    // 1. Показываем кэш сразу, если есть
    const cached = safeGet(CACHE_KEY);
    if (cached?.cards) render(cached.cards);

    // 2. Загружаем список тем
    const forumHTML = await fetchHTML(FORUM_URL);
    const forumDoc = new DOMParser().parseFromString(forumHTML, 'text/html');
    const threads = parseThreadList(forumDoc);

    if (!threads.length) return;

    const currentFirstTitle = threads[0].title.trim();
    const savedFirst = localStorage.getItem(FIRST_KEY);

    // Если первый топик не изменился → выходим
    if (savedFirst === currentFirstTitle) return;

    // Новый топик → обновляем
    localStorage.setItem(FIRST_KEY, currentFirstTitle);
    clearCache();

    // Парсим первые посты
    const cards = await Promise.all(
      threads.map(async t => {
        try {
          const postHTML = await fetchHTML(t.href);
          return { ...t, ...parseFirstPost(postHTML) };
        } catch {
          return { ...t, text: '', img: '', author: '—', date: '—' };
        }
      })
    );

    safeSet(CACHE_KEY, { cards });
    render(cards);

  } catch (err) {
    console.warn('[NF28 Parser]', err);
  }
})();
</script>

Мурчанн

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

Эта версия скрипта более стабильная, не конфликтует с другими скриптами, а защита реализована более гибко.

Код
<section class="sect flex-grow-1">
<div class="sect__header d-flex">
<h2 class="sect__title flex-grow-1">
Forum Updates
<img src="https://jordan.moy.su/forumimages/3droom.png" alt="3D Room Icon" style="height:24px; margin-left:-2px; vertical-align:middle;">
</h2>
</div>
<div class="dark-border">
<div id="nf28_cards"></div>
</div>
</section>

<style>
/* ──────────────── CONTAINER ──────────────── */
#nf28_cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 30px 30px;
  width: 100%;
  padding: 0 12px;
  margin: 0 auto;
  max-width: 1600px;
  box-sizing: border-box;
}

/* ──────────────── CARD ──────────────── */
.nf28Parser_card {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: #fff;
  border-radius: 16px;
  overflow: hidden;
  background: rgba(20, 20, 35, 0.42);
  backdrop-filter: blur(16px) saturate(180%);
  -webkit-backdrop-filter: blur(16px) saturate(180%);
  box-shadow: 0 10px 32px rgba(0,0,0,0.38),
              inset 0 1px 0 rgba(255,255,255,0.07);
  border: 1px solid rgba(255,255,255,0.05);
  transition: transform 0.32s cubic-bezier(0.34, 1.4, 0.64, 1),
              box-shadow 0.32s ease,
              opacity 0.32s ease;
  opacity: 0.97;
}

.nf28Parser_card:hover,
.nf28Parser_card:focus-visible {
  transform: translateY(-8px) scale(1.015);
  box-shadow: 0 20px 50px rgba(0,0,0,0.48),
              inset 0 1px 0 rgba(255,255,255,0.1);
  opacity: 1;
}

/* Image wrapper */
.nf28Parser_img_wrapper {
  position: relative;
  width: 100%;
  height: 240px;
  overflow: hidden;
}

.nf28Parser_img_wrapper img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
}

.nf28Parser_card:hover .nf28Parser_img_wrapper img {
  transform: scale(1.07);
}

/* Author badge */
.nf28Parser_author {
  position: absolute;
  top: 12px;
  left: 12px;
  z-index: 3;
  font-size: 0.86rem;
  font-weight: 700;
  color: #fff;
  background: rgba(0,0,0,0.45);
  backdrop-filter: blur(8px);
  padding: 4px 10px;
  border-radius: 50px;
  border: 1px solid rgba(255,255,220,0.35);
  text-shadow: 0 1px 3px rgba(0,0,0,0.7);
}

/* Title (на картинке) */
.nf28Parser_title {
  position: absolute;
  bottom: 15px;
  left: 0;
  right: 0;
  padding: 16px 16px 28px;
  font-size: clamp(1.1rem, 2.8vw, 1.40rem);
  font-weight: 700;
  line-height: 1.28;
  background: none;
  color: #ffffff;
  text-shadow: 0 2px 12px rgba(0,0,0,1);
  z-index: 2;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

/* Description block */
.nf28Parser_desc_wrapper {
  padding: 16px;
  background: linear-gradient(to bottom, rgba(15,15,25,0.72) 0%, rgba(10,10,20,0.88) 100%);
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.nf28Parser_desc {
  font-size: 1rem;
  line-height: 1.55;
  color: #e0e0f0;
  opacity: 0.9;
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin: 0;
}

/* Footer */
.nf28Parser_footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: auto;
  padding-top: 12px;
  border-top: 1px solid rgba(255,255,255,0.06);
  font-size: 0.87rem;
  color: #c0c0d8;
}

.nf28Parser_date {
  font-weight: 600;
  background: linear-gradient(135deg, #0fd700, #7e1280, #f714d9);
  color: #fff;
  padding: 3px 7px;
  border-radius: 4px;
  font-size: 9.5px;
  text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
  box-shadow: 0 1px 5px rgba(0,0,0,0.2);
}

.nf28Parser_button {
  background: linear-gradient(135deg, #8a5acf 0%, #c06ab8 100%);
  color: white;
  padding: 8px 18px;
  border-radius: 50px;
  font-weight: 700;
  font-size: 0.9rem;
  text-decoration: none;
  transition: all 0.3s ease;
  box-shadow: 0 3px 16px rgba(133,91,151,0.28);
  white-space: nowrap;
}

.nf28Parser_button:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 28px rgba(133,91,151,0.42);
  background: linear-gradient(135deg, #9b6ee0 0%, #d47cc9 100%);
}

/* Gradient overlay */
.nf28Parser_gradient-overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(0,0,0,0.78) 0%, rgba(0,0,0,0.55) 35%, transparent 80%);
  pointer-events: none;
  z-index: 1;
}

/* Responsive */
@media (max-width: 1100px) {
  #nf28_cards {
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: 20px 16px;
  }
}

@media (max-width: 800px) {
  #nf28_cards {
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  }
  .nf28Parser_img_wrapper { height: 200px; }
  .nf28Parser_title {
    font-size: clamp(1.2rem, 4vw, 1.4rem);
    padding: 14px 14px 12px;
  }
  .nf28Parser_desc {
    font-size: 0.95rem;
    -webkit-line-clamp: 3;
  }
}

@media (max-width: 500px) {
  .nf28Parser_img_wrapper { height: 180px; }
}

/* Fade-in animation */
@keyframes nf28ParserFadeIn {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: translateY(0); }
}

.nf28Parser_card {
  animation: nf28ParserFadeIn 0.75s ease-out forwards;
  animation-delay: calc(var(--order, 0) * 100ms);
}
</style>

<script>
(async function nf28ForumParser() {
  /* SINGLE INSTANCE LOCK */
  if (window.__NF28FORUMPARSER_LOCK__) return;
  window.__NF28FORUMPARSER_LOCK__ = true;

  /* ──────────────── CONFIG ──────────────── */
  const CONTAINER_ID = '#nf28_cards';
  const container = document.querySelector(CONTAINER_ID);
  if (!container) return;

  const FORUM_URL = '/forum/0-0-1-34';
  const CACHE_KEY = 'nf28_cards_cache_v4';
  const FIRST_KEY = 'nf28_first_topic_v4';
  const MAX_CARDS = 12;

  /* ──────────────── UTILS ──────────────── */
  const toAbs = url => !url ? '' :
    url.startsWith('http') ? url :
    url.startsWith('//') ? location.protocol + url :
    location.origin + (url.startsWith('/') ? url : '/' + url);

  const esc = text => String(text || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"','\'':'''}[m]));

  const safeGet = key => { try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch { return null; }};
  const safeSet = (key, val) => { try { localStorage.setItem(key, JSON.stringify(val)); } catch {} };
  const clearCache = () => localStorage.removeItem(CACHE_KEY);

  async function fetchHTML(url) {
    try {
      const r = await fetch(url, { credentials: 'same-origin', cache: 'no-cache' });
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      return await r.text();
    } catch (e) {
      throw e;
    }
  }

  /* ──────────────── CARD CREATION (НОВЫЕ КЛАССЫ) ──────────────── */
  function createCard({ href, img, title, text, author, date }) {
    const card = document.createElement('a');
    card.className = 'nf28Parser_card';
    card.href = href;
    card.target = '_self';
    card.innerHTML = `
      <div class="nf28Parser_img_wrapper">
        <img src="${img || '/forumimages/Newsletter-3.png'}" alt="">
        <div class="nf28Parser_gradient-overlay"></div>
        <div class="nf28Parser_author">${esc(author || 'Автор')}</div>
        <div class="nf28Parser_title">${esc(title || 'Без названия')}</div>
      </div>
      <div class="nf28Parser_desc_wrapper">
        <div class="nf28Parser_desc">${esc(text || '').slice(0, 320)}</div>
        <div class="nf28Parser_footer">
          <span class="nf28Parser_date">${esc(date || 'Дата')}</span>
          <a href="${href}" class="nf28Parser_button">Далее</a>
        </div>
      </div>`;
    return card;
  }

  function render(cards) {
    if (!cards?.length) return;
    container.innerHTML = '';
    cards.forEach((c, i) => {
      const el = createCard(c);
      el.style.setProperty('--order', i);
      container.appendChild(el);
    });
  }

  /* ──────────────── PARSERS (УЛУЧШЕННЫЕ) ──────────────── */
  function parseThreadList(doc) {
    const items = doc.querySelectorAll('.threadNametd .threadLink');
    return Array.from(items).slice(0, MAX_CARDS).map(a => ({
      href: toAbs(a.getAttribute('href') || ''),
      title: a.textContent.trim()
    }));
  }

  function parseFirstPost(html) {
    const doc = new DOMParser().parseFromString(html, 'text/html');
    let text = '', img = '', author = 'Автор', date = 'Дата';

    // Контент первого поста
    const post = doc.querySelector('.post_content, .post_body, .post, .ipsType_richText, .cPost_content, article.cPost');
    if (post) {
      text = post.textContent.trim().replace(/\s+/g, ' ');
      const imgs = post.querySelectorAll('img[src]');
      for (let im of imgs) {
        const src = im.getAttribute('src') || '';
        if (src && !/avatar|smiley|icon|spacer/i.test(src)) {
          img = toAbs(src);
          break;
        }
      }
    }

    // === АВТОР (самая частая проблема решена) ===
    const authorSelectors = [
      '.cAuthorPane_author', '.ipsComment_author', '.postUser', '.author_name',
      '[class*="author"]', '[class*="user"] a'
    ];
    for (let sel of authorSelectors) {
      const el = doc.querySelector(sel);
      if (el) {
        author = el.textContent.trim();
        break;
      }
    }
    // запасной вариант — первая ссылка на профиль
    if (author === 'Автор') {
      const profile = doc.querySelector('a[href*="/profile/"], a[href*="/members/"], a[href*="/user/"]');
      if (profile) author = profile.textContent.trim();
    }
    author = author.replace(/^\s*Автор:\s*/i, '').trim();
    if (!author || author.length < 2) author = 'Автор';

    // === ДАТА ===
    const timeEl = doc.querySelector('time[datetime], .post_date time, .ipsComment_meta time, .cPost_time');
    if (timeEl) {
      const dt = timeEl.getAttribute('datetime') || timeEl.textContent;
      const d = new Date(dt);
      if (!isNaN(d.getTime())) {
        date = d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: 'numeric' });
      } else date = dt.trim();
    } else {
      // старый надёжный fallback
      const els = [...doc.querySelectorAll('td, div, span, p')];
      for (let el of els) {
        const txt = el.textContent;
        if (/Дата:|Опубликовано:|Добавлено:|Сегодня|Вчера/i.test(txt)) {
          const m = txt.match(/(?:Дата|Опубликовано|Добавлено):\s*([^|\n<]+)/i) ||
                    txt.match(/(\d{1,2}\.\d{1,2}\.\d{4}|\d{1,2} [а-яё]+ \d{4})/i);
          if (m) { date = m[1] || m[0]; break; }
        }
      }
    }

    return { text: text.slice(0, 320), img, author, date };
  }

  /* ──────────────── LOGIC ──────────────── */
  try {
    // 1. Показываем кэш сразу (поддержка старого v3)
    let cached = safeGet(CACHE_KEY);
    if (!cached?.cards) cached = safeGet('nf28_cards_cache_v3'); // плавный переход

    if (cached?.cards) render(cached.cards);

    // 2. Проверяем, есть ли новые темы
    const forumHTML = await fetchHTML(FORUM_URL);
    const forumDoc = new DOMParser().parseFromString(forumHTML, 'text/html');
    const threads = parseThreadList(forumDoc);

    if (!threads.length) return;

    const currentFirstTitle = threads[0].title.trim();
    const savedFirst = localStorage.getItem(FIRST_KEY);

    if (savedFirst === currentFirstTitle) return; // ничего нового

    // Новый топик → обновляем
    localStorage.setItem(FIRST_KEY, currentFirstTitle);
    clearCache();

    // Парсим все первые посты с новым улучшенным парсером
    const cards = await Promise.all(
      threads.map(async t => {
        try {
          const postHTML = await fetchHTML(t.href);
          return { ...t, ...parseFirstPost(postHTML) };
        } catch {
          return { ...t, text: '', img: '', author: '—', date: '—' };
        }
      })
    );

    safeSet(CACHE_KEY, { cards });
    // Удаляем старый кэш v3, чтобы не мешал
    localStorage.removeItem('nf28_cards_cache_v3');

    render(cards);

  } catch (err) {
    console.warn('[NF28 Parser v4]', err);
    // Если оффлайн — просто остаётся кэш (даже старый v3 покажет)
  }
})();
</script>

Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
Дата: Пятница, 27.02.2026, 12:50 | Сообщение # 3 | | Написал: Узнаваемый
Автор темы
Мурчанн не в сети
        Сообщений:211
         Регистрация:20.10.2016

Это новые стили, в которых заголовок карточки прижат максимально к низу.

Парсируются только четыре карточки.

Код
<section class="sect flex-grow-1">
<div class="sect__header d-flex">
<h2 class="sect__title flex-grow-1">
Forum Updates
<img src="https://jordan.moy.su/forumimages/3droom.png" alt="3D Room Icon" style="height:24px; margin-left:-2px; vertical-align:middle;">
</h2>
</div>
<div class="dark-border">
<div id="nf28_cards"></div>
</div>
</section>

<style>
/* ──────────────── CONTAINER ──────────────── */
#nf28_cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 30px 30px;
  width: 100%;
  padding: 0 12px;
  margin: 0 auto;
  max-width: 1600px;
  box-sizing: border-box;
}

/* ──────────────── CARD ──────────────── */
.nf28Parser_card {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: #fff;
  border-radius: 16px;
  overflow: hidden;
  background: rgba(20, 20, 35, 0.42);
  backdrop-filter: blur(16px) saturate(180%);
  -webkit-backdrop-filter: blur(16px) saturate(180%);
  box-shadow: 0 10px 32px rgba(0,0,0,0.38),
              inset 0 1px 0 rgba(255,255,255,0.07);
  border: 1px solid rgba(255,255,255,0.05);
  transition: transform 0.32s cubic-bezier(0.34, 1.4, 0.64, 1),
              box-shadow 0.32s ease,
              opacity 0.32s ease;
  opacity: 0.97;
}

.nf28Parser_card:hover,
.nf28Parser_card:focus-visible {
  transform: translateY(-8px) scale(1.015);
  box-shadow: 0 20px 50px rgba(0,0,0,0.48),
              inset 0 1px 0 rgba(255,255,255,0.1);
  opacity: 1;
}

/* ──────────────── IMAGE WRAPPER ──────────────── */
.nf28Parser_img_wrapper {
  position: relative;
  width: 100%;
  height: 240px;
  overflow: hidden;

  display: flex;
  flex-direction: column;
  justify-content: flex-end; /* заголовок прижат к низу */
}

/* Background image */
.nf28Parser_img_wrapper img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
  z-index: 0;
}

.nf28Parser_card:hover .nf28Parser_img_wrapper img {
  transform: scale(1.07);
}

/* Gradient overlay */
.nf28Parser_gradient-overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top,
    rgba(0,0,0,0.85) 0%,
    rgba(0,0,0,0.6) 35%,
    rgba(0,0,0,0.2) 65%,
    transparent 100%);
  pointer-events: none;
  z-index: 1;
}

/* Author badge */
.nf28Parser_author {
  position: absolute;
  top: 12px;
  left: 12px;
  z-index: 3;
  font-size: 0.86rem;
  font-weight: 700;
  color: #fff;
  background: rgba(0,0,0,0.45);
  backdrop-filter: blur(8px);
  padding: 4px 10px;
  border-radius: 50px;
  border: 1px solid rgba(255,255,220,0.35);
  text-shadow: 0 1px 3px rgba(0,0,0,0.7);
}

/* ──────────────── TITLE FIX ──────────────── */
.nf28Parser_title {
  position: relative; /* больше не absolute */
  margin: 0;
  padding: 6px 12px;
  z-index: 2;

  font-size: clamp(1.1rem, 2.8vw, 1.4rem);
  font-weight: 700;
  line-height: 1.25;
  color: #fff;
  text-shadow: 0 3px 12px rgba(0,0,0,0.95);

  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
  -webkit-line-clamp: 2;
}

/* ──────────────── DESCRIPTION ──────────────── */
.nf28Parser_desc_wrapper {
  padding: 16px;
  background: linear-gradient(to bottom, rgba(15,15,25,0.72), rgba(10,10,20,0.88));
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.nf28Parser_desc {
  font-size: 1rem;
  line-height: 1.55;
  color: #e0e0f0;
  opacity: 0.9;
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin: 0;
}

/* ──────────────── FOOTER ──────────────── */
.nf28Parser_footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: auto;
  padding-top: 12px;
  border-top: 1px solid rgba(255,255,255,0.06);
  font-size: 0.87rem;
  color: #c0c0d8;
}

.nf28Parser_date {
  font-weight: 600;
  background: linear-gradient(135deg,#0fd700,#7e1280,#f714d9);
  color: #fff;
  padding: 3px 7px;
  border-radius: 4px;
  font-size: 9.5px;
}

.nf28Parser_button {
  background: linear-gradient(135deg,#8a5acf 0%,#c06ab8 100%);
  color: white;
  padding: 8px 18px;
  border-radius: 50px;
  font-weight: 700;
  font-size: 0.9rem;
  text-decoration: none;
  transition: all 0.3s ease;
  box-shadow: 0 3px 16px rgba(133,91,151,0.28);
  white-space: nowrap;
}

.nf28Parser_button:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 28px rgba(133,91,151,0.42);
  background: linear-gradient(135deg, #9b6ee0 0%, #d47cc9 100%);
}

/* ──────────────── RESPONSIVE ──────────────── */
@media (max-width:1100px){
  #nf28_cards{
    grid-template-columns: repeat(auto-fill,minmax(260px,1fr));
    gap:20px;
  }
}
@media (max-width:800px){
  #nf28_cards{
    grid-template-columns: repeat(auto-fill,minmax(240px,1fr));
  }
  .nf28Parser_img_wrapper{ height:200px; }
}
@media (max-width:500px){
  .nf28Parser_img_wrapper{ height:180px; }
}

/* ──────────────── FADE-IN ──────────────── */
@keyframes nf28ParserFadeIn {
  from { opacity: 0; transform: translateY(24px); }
  to { opacity: 1; transform: translateY(0); }
}

.nf28Parser_card {
  animation: nf28ParserFadeIn 0.75s ease-out forwards;
  animation-delay: calc(var(--order,0) * 100ms);
}
</style>

<script>
(async function nf28ForumParser() {
/* SINGLE INSTANCE LOCK */
if (window.__NF28FORUMPARSER_LOCK__) return;
window.__NF28FORUMPARSER_LOCK__ = true;

/* ──────────────── CONFIG ──────────────── */
const CONTAINER_ID = '#nf28_cards';
const container = document.querySelector(CONTAINER_ID);
if (!container) return;

const FORUM_URL = '/forum/0-0-1-34';
const CACHE_KEY = 'nf28_cards_cache_v4';
const FIRST_KEY = 'nf28_first_topic_v4';
const MAX_CARDS = 4;

/* ──────────────── UTILS ──────────────── */
const toAbs = url => !url ? '' :
url.startsWith('http') ? url :
url.startsWith('//') ? location.protocol + url :
location.origin + (url.startsWith('/') ? url : '/' + url);

const esc = text => String(text || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"','\'':'''}[m]));

const safeGet = key => { try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch { return null; }};
const safeSet = (key, val) => { try { localStorage.setItem(key, JSON.stringify(val)); } catch {} };
const clearCache = () => localStorage.removeItem(CACHE_KEY);

async function fetchHTML(url) {
try {
const r = await fetch(url, { credentials: 'same-origin', cache: 'no-cache' });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return await r.text();
} catch (e) {
throw e;
}
}

/* ──────────────── CARD CREATION (НОВЫЕ КЛАССЫ) ──────────────── */
function createCard({ href, img, title, text, author, date }) {
const card = document.createElement('a');
card.className = 'nf28Parser_card';
card.href = href;
card.target = '_self';
card.innerHTML = `
<div class="nf28Parser_img_wrapper">
<img src="${img || '/forumimages/Newsletter-3.png'}" alt="">
<div class="nf28Parser_gradient-overlay"></div>
<div class="nf28Parser_author">${esc(author || 'Автор')}</div>
<div class="nf28Parser_title">${esc(title || 'Без названия')}</div>
</div>
<div class="nf28Parser_desc_wrapper">
<div class="nf28Parser_desc">${esc(text || '').slice(0, 320)}</div>
<div class="nf28Parser_footer">
<span class="nf28Parser_date">${esc(date || 'Дата')}</span>
<a href="${href}" class="nf28Parser_button">Далее</a>
</div>
</div>`;
return card;
}

function render(cards) {
if (!cards?.length) return;
container.innerHTML = '';
cards.forEach((c, i) => {
const el = createCard(c);
el.style.setProperty('--order', i);
container.appendChild(el);
});
}

/* ──────────────── PARSERS (УЛУЧШЕННЫЕ) ──────────────── */
function parseThreadList(doc) {
const items = doc.querySelectorAll('.threadNametd .threadLink');
return Array.from(items).slice(0, MAX_CARDS).map(a => ({
href: toAbs(a.getAttribute('href') || ''),
title: a.textContent.trim()
}));
}

function parseFirstPost(html) {
const doc = new DOMParser().parseFromString(html, 'text/html');
let text = '', img = '', author = 'Автор', date = 'Дата';

// Контент первого поста
const post = doc.querySelector('.post_content, .post_body, .post, .ipsType_richText, .cPost_content, article.cPost');
if (post) {
text = post.textContent.trim().replace(/\s+/g, ' ');
const imgs = post.querySelectorAll('img[src]');
for (let im of imgs) {
const src = im.getAttribute('src') || '';
if (src && !/avatar|smiley|icon|spacer/i.test(src)) {
img = toAbs(src);
break;
}
}
}

// === АВТОР (самая частая проблема решена) ===
const authorSelectors = [
'.cAuthorPane_author', '.ipsComment_author', '.postUser', '.author_name',
'[class*="author"]', '[class*="user"] a'
];
for (let sel of authorSelectors) {
const el = doc.querySelector(sel);
if (el) {
author = el.textContent.trim();
break;
}
}
// запасной вариант — первая ссылка на профиль
if (author === 'Автор') {
const profile = doc.querySelector('a[href*="/profile/"], a[href*="/members/"], a[href*="/user/"]');
if (profile) author = profile.textContent.trim();
}
author = author.replace(/^\s*Автор:\s*/i, '').trim();
if (!author || author.length < 2) author = 'Автор';

// === ДАТА ===
const timeEl = doc.querySelector('time[datetime], .post_date time, .ipsComment_meta time, .cPost_time');
if (timeEl) {
const dt = timeEl.getAttribute('datetime') || timeEl.textContent;
const d = new Date(dt);
if (!isNaN(d.getTime())) {
date = d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: 'numeric' });
} else date = dt.trim();
} else {
// старый надёжный fallback
const els = [...doc.querySelectorAll('td, div, span, p')];
for (let el of els) {
const txt = el.textContent;
if (/Дата:|Опубликовано:|Добавлено:|Сегодня|Вчера/i.test(txt)) {
const m = txt.match(/(?:Дата|Опубликовано|Добавлено):\s*([^|\n<]+)/i) ||
txt.match(/(\d{1,2}\.\d{1,2}\.\d{4}|\d{1,2} [а-яё]+ \d{4})/i);
if (m) { date = m[1] || m[0]; break; }
}
}
}

return { text: text.slice(0, 320), img, author, date };
}

/* ──────────────── LOGIC ──────────────── */
try {
// 1. Показываем кэш сразу (поддержка старого v3)
let cached = safeGet(CACHE_KEY);
if (!cached?.cards) cached = safeGet('nf28_cards_cache_v3'); // плавный переход

if (cached?.cards) render(cached.cards);

// 2. Проверяем, есть ли новые темы
const forumHTML = await fetchHTML(FORUM_URL);
const forumDoc = new DOMParser().parseFromString(forumHTML, 'text/html');
const threads = parseThreadList(forumDoc);

if (!threads.length) return;

const currentFirstTitle = threads[0].title.trim();
const savedFirst = localStorage.getItem(FIRST_KEY);

if (savedFirst === currentFirstTitle) return; // ничего нового

// Новый топик → обновляем
localStorage.setItem(FIRST_KEY, currentFirstTitle);
clearCache();

// Парсим все первые посты с новым улучшенным парсером
const cards = await Promise.all(
threads.map(async t => {
try {
const postHTML = await fetchHTML(t.href);
return { ...t, ...parseFirstPost(postHTML) };
} catch {
return { ...t, text: '', img: '', author: '—', date: '—' };
}
})
);

safeSet(CACHE_KEY, { cards });
// Удаляем старый кэш v3, чтобы не мешал
localStorage.removeItem('nf28_cards_cache_v3');

render(cards);

} catch (err) {
console.warn('[NF28 Parser v4]', err);
// Если оффлайн — просто остаётся кэш (даже старый v3 покажет)
}
})();
</script>

Мурчанн

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