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

1
Админ
Постов: 288
2
VIP
Постов: 75
3
Элита
Постов: 62
4
Проверенные
Постов: 46
5
Проверенные
Постов: 45
6
Проверенные
Постов: 40
7
VIP
Постов: 36
8
VIP
Постов: 35

  • Страница 1 из 1
  • 1
Парсер ленты форума на главную страницу для uCoz V 6.0
Дата: Суббота, 21.02.2026, 18:15 | Сообщение # 1 | | Написал: Известный
Автор темы
Мурчанн не в сети
        Сообщений:288
         Регистрация: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 | | Написал: Известный
Автор темы
Мурчанн не в сети
        Сообщений:288
         Регистрация: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 | | Написал: Известный
Автор темы
Мурчанн не в сети
        Сообщений:288
         Регистрация: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
Поиск: