Парсер ленты форума на главную страницу для uCoz V 6.0
Дата: Суббота, 21.02.2026, 18:15 | Сообщение # 1 |
|
Написал: Узнаваемый
Автор темы
Мурчанн
не в сети
Сообщений: 211
Вживую его можно увидеть на главной странице. Это оригинальный парсер. Давно хотел создать подобный вариант именно в плане дизайна. Логика скрипта не менялась, используется всё тот же самый код, никаких изменений не вносилось. Работает исправно, багов не обнаружено.Создаём глобальный блок «Лента» и вставляем в него парсер. Код
<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
Эта версия скрипта более стабильная, не конфликтует с другими скриптами, а защита реализована более гибко.Код
<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
Это новые стили, в которых заголовок карточки прижат максимально к низу. Парсируются только четыре карточки.Код
<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>
Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.