This commit is contained in:
Tomáš Dvořák
2025-09-23 20:15:36 +02:00
parent b8891c8a38
commit 71942e45b9
49 changed files with 8453 additions and 929 deletions
+74
View File
@@ -0,0 +1,74 @@
'use strict';
(function(){
const KEY = 'adminAuthB64'; // stores base64 of user:pass
function b64(u, p){
try { return btoa(`${u}:${p}`); } catch { return ''; }
}
function get(){
try { return localStorage.getItem(KEY) || ''; } catch { return ''; }
}
function set(val){
try { localStorage.setItem(KEY, val||''); } catch {}
}
function clear(){
try { localStorage.removeItem(KEY); } catch {}
}
window.AdminAuth = {
has(){ return !!get(); },
getHeaders(){ const v = get(); return v ? { 'Authorization': 'Basic '+v } : {}; },
setCreds(user, pass){ set(b64(user, pass)); },
clear(){ clear(); }
};
// small UI helper (optional) appears bottom-left
function ensureWidget(){
if (document.getElementById('admin-auth-widget')) return;
const wrap = document.createElement('div');
wrap.id = 'admin-auth-widget';
wrap.style.position = 'fixed';
wrap.style.left = '12px';
wrap.style.bottom = '12px';
wrap.style.zIndex = '9999';
wrap.style.display = 'flex';
wrap.style.gap = '6px';
const btnSet = document.createElement('button');
btnSet.textContent = 'Přihlásit';
btnSet.style.padding = '6px 10px';
btnSet.style.borderRadius = '8px';
btnSet.style.border = '1px solid #cbd5e1';
btnSet.style.background = '#fff';
btnSet.addEventListener('click', () => {
const u = prompt('Uživatel (e-mail):');
if (!u) return;
const p = prompt('Heslo:');
if (p == null) return;
window.AdminAuth.setCreds(u, p);
alert('Přihlašovací údaje uloženy do tohoto prohlížeče.');
});
const btnClr = document.createElement('button');
btnClr.textContent = 'Odhlásit';
btnClr.style.padding = '6px 10px';
btnClr.style.borderRadius = '8px';
btnClr.style.border = '1px solid #cbd5e1';
btnClr.style.background = '#fff';
btnClr.addEventListener('click', () => {
window.AdminAuth.clear();
alert('Odhlášeno uložené údaje odstraněny.');
});
wrap.appendChild(btnSet);
wrap.appendChild(btnClr);
document.body.appendChild(wrap);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', ensureWidget);
} else {
ensureWidget();
}
})();
+89
View File
@@ -0,0 +1,89 @@
'use strict';
(function(){
function h(el, attrs={}, children=[]) {
const e = document.createElement(el);
for (const [k,v] of Object.entries(attrs||{})) {
if (k === 'class') e.className = v; else if (k === 'html') e.innerHTML = v; else e.setAttribute(k, v);
}
for (const c of (children||[])) e.appendChild(c);
return e;
}
function renderItem(item){
const col = h('div', {class: 'items col-xl-6 col-lg-6 col-md-6 col-sm-6 col-ms-6 col-xs-12'});
const article = h('article', {class: 'post-25620 post type-post status-publish format-standard has-post-thumbnail hentry'});
const aPhoto = h('a', {href: item.link, class: 'lte-photo'});
const img = h('img', {
src: item.image,
width: '500',
height: '300',
decoding: 'async',
fetchpriority: 'high',
class: 'attachment-atleticos-blog size-atleticos-blog wp-post-image',
alt: ''
});
aPhoto.appendChild(img);
aPhoto.appendChild(h('span', {class: 'lte-photo-overlay'}));
const descr = h('div', {class: 'lte-description'});
const aHeader = h('a', {href: item.link, class: 'lte-header'});
const h3 = h('h3', {html: item.title || ('Článek ' + item.id)});
aHeader.appendChild(h3);
// const excerpt = h('div', {class: 'lte-excerpt', html: ''});
descr.appendChild(aHeader);
// descr.appendChild(excerpt);
article.appendChild(aPhoto);
article.appendChild(descr);
col.appendChild(article);
return col;
}
async function loadLatest(attempt = 0) {
const primary = document.getElementById('latest-blog-items');
const secondary = document.getElementById('other-blog-items');
if (!primary && !secondary) return;
if (primary) primary.innerHTML = '<div style="width:100%;text-align:center;padding:12px;color:#888;">Načítání…</div>';
if (secondary) secondary.innerHTML = '';
try {
const res = await fetch('/api/blog/latest?limit=12', {credentials: 'omit'});
if (!res.ok) throw new Error('HTTP '+res.status);
let items = await res.json();
if (primary) primary.innerHTML = '';
if (!Array.isArray(items) || items.length === 0) {
if (primary) primary.innerHTML = '<div style="width:100%;text-align:center;padding:12px;color:#888;">Žádné příspěvky zatím nejsou.</div>';
return;
}
// Exclude carousel-used posts and pinned 0000 from the 4-grid
const hasCarouselIds = Array.isArray(window.CAROUSEL_BLOG_IDS) && window.CAROUSEL_BLOG_IDS.length > 0;
// If carousel IDs are not ready yet, retry shortly (max 5 attempts)
if (!hasCarouselIds && attempt < 5) {
setTimeout(() => loadLatest(attempt + 1), 300);
return;
}
const excluded = new Set(hasCarouselIds ? window.CAROUSEL_BLOG_IDS : []);
const four = items.filter(it => it && it.id !== '0000' && !excluded.has(it.id)).slice(0, 4);
// Prefer rendering into primary; keep secondary empty to avoid duplicates
const target = primary || secondary;
if (target) {
const frag = document.createDocumentFragment();
four.forEach(it => frag.appendChild(renderItem(it)));
target.appendChild(frag);
}
if (secondary && secondary !== target) secondary.innerHTML = '';
} catch (e) {
console.error('Load latest blog error', e);
if (primary) primary.innerHTML = '<div style="width:100%;text-align:center;padding:12px;color:#c00;">Nepodařilo se načíst novinky.</div>';
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => loadLatest(0));
} else {
loadLatest(0);
}
})();
+85
View File
@@ -0,0 +1,85 @@
'use strict';
(function(){
function h(el, attrs={}, children=[]) {
const e = document.createElement(el);
for (const [k,v] of Object.entries(attrs||{})) {
if (k === 'class') e.className = v; else if (k === 'html') e.innerHTML = v; else e.setAttribute(k, v);
}
for (const c of (children||[])) e.appendChild(c);
return e;
}
function renderItem(item){
const col = h('div', {class: 'col-xl-4 col-lg-6 col-md-6 col-sm-12 col-xs-12 item div-thumbnail'});
const article = h('article', {class: 'post-25620 post type-post status-publish format-standard has-post-thumbnail hentry'});
const aPhoto = h('a', {href: item.link, class: 'lte-photo'});
const img = h('img', {
src: item.image,
width: '500', height: '300', decoding: 'async', fetchpriority: 'high',
class: 'attachment-atleticos-blog size-atleticos-blog wp-post-image', alt: ''
});
aPhoto.appendChild(img);
aPhoto.appendChild(h('span', {class: 'lte-photo-overlay'}));
const descr = h('div', {class: 'lte-description'});
const aHeader = h('a', {href: item.link, class: 'lte-header'});
aHeader.appendChild(h('h3', {html: item.title || ('Článek ' + item.id)}));
descr.appendChild(aHeader);
article.appendChild(aPhoto);
article.appendChild(descr);
col.appendChild(article);
return col;
}
function numericDesc(a,b){
const ai = parseInt(a.id,10); const bi = parseInt(b.id,10);
if (!isNaN(ai) && !isNaN(bi)) return bi - ai;
return (b.id||'').localeCompare(a.id||'');
}
function getQueryParam(name){
const url = new URL(window.location.href);
return url.searchParams.get(name);
}
function normalize(s){ return (s||'').toString().trim().toLowerCase(); }
async function loadAll(){
// Render into the primary masonry grid, overwriting any static items
const mount = document.querySelector('.lte-blog-wrap .blog .row.masonry');
if (!mount) return;
mount.innerHTML = '<div style="width:100%;text-align:center;padding:12px;color:#888;">Načítání…</div>';
try {
const res = await fetch('/api/blog/latest?limit=10000', {credentials: 'omit'});
if (!res.ok) throw new Error('HTTP '+res.status);
let items = await res.json();
if (!Array.isArray(items)) items = [];
// Sort by numeric ID desc to ensure largest number is latest
items.sort(numericDesc);
// Optional filter by category via ?category=XYZ
const qCat = getQueryParam('category');
if (qCat) {
const want = normalize(qCat);
items = items.filter(it => Array.isArray(it.categories) && it.categories.some(c => normalize(c) === want));
}
mount.innerHTML = '';
if (items.length === 0) {
mount.innerHTML = '<div style="width:100%;text-align:center;padding:12px;color:#888;">Žádné příspěvky zatím nejsou.</div>';
return;
}
const frag = document.createDocumentFragment();
items.forEach(it => frag.appendChild(renderItem(it)));
mount.appendChild(frag);
} catch (e) {
console.error('Load blog list error', e);
mount.innerHTML = '<div style="width:100%;text-align:center;padding:12px;color:#c00;">Nepodařilo se načíst seznam článků.</div>';
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadAll);
} else {
loadAll();
}
})();
+67
View File
@@ -0,0 +1,67 @@
'use strict';
(function(){
async function updateHeroFromLatest() {
try {
const res = await fetch('/api/blog/latest?limit=8', {credentials:'omit'});
if (!res.ok) throw new Error('HTTP '+res.status);
let items = await res.json();
if (!Array.isArray(items) || items.length === 0) items = [];
// Update background images of zoom slider slides if present
const slides = document.querySelectorAll('.lte-slider-zoom .zs-slides .zs-slide');
// If slides are not yet initialized by the plugin, try again shortly
if (!slides || slides.length === 0) {
setTimeout(updateHeroFromLatest, 600);
return;
}
// Track which IDs are used in the carousel
const usedIds = [];
// Slide 0 is always the intro post 0000
const slide0 = slides[0];
if (slide0) {
usedIds.push('0000');
slide0.style.backgroundImage = `url('img/blog/0000.png')`;
const content0 = document.querySelector(`.lte-zs-slider-inner.lte-zs-slide-0`);
if (content0) {
const btn0 = content0.querySelector('a.lte-btn');
if (btn0) btn0.setAttribute('href', 'blog/0000.html');
// Do not alter existing H2 text; designers may have custom text
}
}
// Fill remaining slides with latest posts, skipping 0000
const rest = items.filter(it => it && it.id !== '0000');
const max = Math.min(rest.length, Math.max(0, slides.length - 1));
for (let i = 0; i < max; i++) {
const it = rest[i];
if (it && it.id) usedIds.push(it.id);
const slide = slides[i+1];
if (!slide) continue;
slide.style.backgroundImage = `url('${it.image}')`;
const content = document.querySelector(`.lte-zs-slider-inner.lte-zs-slide-${i+1}`);
if (content) {
const header = content.querySelector('h2.lte-header');
if (header) header.textContent = it.title || '';
const btn = content.querySelector('a.lte-btn');
if (btn) btn.setAttribute('href', it.link);
}
}
// Expose used IDs so other widgets can exclude them (e.g., 4-post grid)
window.CAROUSEL_BLOG_IDS = usedIds;
} catch (e) {
console.error('home-autofill hero update error', e);
}
}
function onReady(fn){
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); else fn();
}
onReady(() => {
// Delay a bit to allow zoomslider to initialize, then update
setTimeout(updateHeroFromLatest, 500);
});
})();
+71 -6
View File
@@ -2,13 +2,15 @@
jQuery( function() {
initEvents();
initEvents();
initStyles();
initCollapseMenu();
checkCountUp();
initScrollReveal();
checkScrollAnimation();
initStyles();
sanitizeBlogContent();
cleanupDuplicateBlocks();
initCollapseMenu();
checkCountUp();
initScrollReveal();
checkScrollAnimation();
});
jQuery(window).on('scroll', function (event) {
@@ -71,6 +73,23 @@ function initCollapseMenu() {
return false;
}
// Remove duplicate footers and duplicate blog grids (defensive cleanup)
function cleanupDuplicateBlocks(){
try {
// Keep only the first footer wrapper
var footers = document.querySelectorAll('.lte-footer-wrapper');
for (var i = 1; i < footers.length; i++) {
footers[i].remove();
}
// Keep only the first blog grid inside main blog wrap
var grids = document.querySelectorAll('.lte-blog-wrap .blog.blog-block');
for (var j = 1; j < grids.length; j++) {
grids[j].remove();
}
} catch(e) { /* no-op */ }
}
}
});
@@ -86,6 +105,52 @@ function initCollapseMenu() {
});
}
// Remove empty/garbage paragraphs from blog content and normalize spacing
function sanitizeBlogContent(){
try {
var blocks = document.querySelectorAll('.text.lte-text-page.clearfix');
blocks.forEach(function(block){
// remove <p> that are empty or contain only <br> or whitespace
Array.from(block.querySelectorAll('p')).forEach(function(p){
var html = p.innerHTML.trim();
var text = p.textContent.replace(/\u00A0/g,' ').trim();
if (!text || /^<br\s*\/?>(\s|&nbsp;)*$/i.test(html)) {
p.remove();
return;
}
// remove hashtag-only paragraphs (e.g., #tag #tag2)
var anchors = Array.from(p.querySelectorAll('a'));
var allHashLinks = anchors.length > 0 && anchors.every(function(a){
var t = (a.textContent||'').trim();
return t.startsWith('#');
});
var pureHashText = /^#\S+(?:\s+#\S+)*$/.test(text);
if (allHashLinks || pureHashText) {
p.remove();
return;
}
// strip excessive bottom margins coming from pasted content
p.style.marginBottom = '';
p.style.marginTop = '';
});
// collapse consecutive <p> duplicates with same text
var prevText = null;
Array.from(block.querySelectorAll('p')).forEach(function(p){
var t = p.textContent.trim();
if (prevText !== null && t === prevText) {
p.remove();
} else {
prevText = t;
}
});
});
// Remove tags/sharing/related containers if still present in DOM
document.querySelectorAll('.blog-info-post-bottom, .tags-line, .lte-sharing, .lte-related').forEach(function(el){
el.remove();
});
} catch(e) { /* no-op */ }
}
/* Navbar attributes with dependency on resolution and scroll status */
function checkNavbar() {
+126
View File
@@ -0,0 +1,126 @@
'use strict';
(function(){
function h(el, attrs={}, children=[]) {
const e = document.createElement(el);
for (const [k,v] of Object.entries(attrs||{})) {
if (k === 'class') e.className = v; else if (k === 'html') e.innerHTML = v; else e.setAttribute(k, v);
}
for (const c of (children||[])) e.appendChild(c);
return e;
}
function ytUrl(videoId){
return 'https://www.youtube.com/watch?v=' + videoId;
}
function renderFeatured(v) {
const article = h('article', {class: 'post format-video has-post-thumbnail hentry'});
const wrap = h('div', {class: 'lte-wrapper'});
const a = h('a', {href: ytUrl(v.video_id), target: '_blank', class: 'lte-photo lte-video-popup swipebox'});
const img = h('img', {loading: 'lazy', decoding: 'async', width: '1600', height: '969', src: v.thumbnail_url, class: 'attachment-full size-full wp-post-image', alt: ''});
const iconWrap = h('span', {class: 'lte-icon-video'});
iconWrap.appendChild(h('ion-icon', {name: 'play-circle-outline', size: 'large'}));
iconWrap.appendChild(h('span', {html: v.length || ''}));
a.appendChild(img);
a.appendChild(iconWrap);
wrap.appendChild(a);
const descr = h('div', {class: 'lte-description'});
const dateTop = h('span', {class: 'lte-date-top'});
const dateA = h('a', {href: '', class: 'lte-date'});
dateA.appendChild(h('span', {class: 'dt', html: v.published_text || v.published_date || ''}));
dateTop.appendChild(dateA);
const headerA = h('a', {href: ytUrl(v.video_id), class: 'lte-header', target: '_blank'});
headerA.appendChild(h('h3', {html: v.title || ''}));
descr.appendChild(dateTop);
descr.appendChild(headerA);
// keep layout spacing consistent
descr.appendChild(h('div', {class: 'lte-excerpt'}));
article.appendChild(wrap);
article.appendChild(descr);
return article;
}
function renderGridItem(v){
const col = h('div', {class: 'items col-xl-6 col-lg-6 col-md-6 col-sm-6 col-ms-12 col-xs-12'});
const article = h('article', {class: 'post format-video has-post-thumbnail hentry'});
const wrap = h('div', {class: 'lte-wrapper'});
const a = h('a', {href: ytUrl(v.video_id), target: '_blank', class: 'lte-photo lte-video-popup swipebox'});
const img = h('img', {loading: 'lazy', decoding: 'async', width: '1600', height: '969', src: v.thumbnail_url, class: 'attachment-full size-full wp-post-image', alt: ''});
const iconWrap = h('span', {class: 'lte-icon-video'});
iconWrap.appendChild(h('ion-icon', {name: 'play-circle-outline', size: 'large'}));
iconWrap.appendChild(h('span', {html: v.length || ''}));
a.appendChild(img);
a.appendChild(iconWrap);
wrap.appendChild(a);
const descr = h('div', {class: 'lte-description'});
const dateTop = h('span', {class: 'lte-date-top'});
const dateA = h('a', {href: '', class: 'lte-date'});
dateA.appendChild(h('span', {class: 'dt', html: v.published_text || v.published_date || ''}));
dateTop.appendChild(dateA);
const headerA = h('a', {href: ytUrl(v.video_id), class: 'lte-header', target: '_blank'});
headerA.appendChild(h('h3', {html: v.title || ''}));
descr.appendChild(dateTop);
descr.appendChild(headerA);
// keep layout spacing consistent
descr.appendChild(h('div', {class: 'lte-excerpt'}));
article.appendChild(wrap);
article.appendChild(descr);
col.appendChild(article);
return col;
}
async function loadVideos(){
const featureMount = document.getElementById('latest-video-feature');
const gridMount = document.getElementById('latest-videos-grid');
if (!featureMount && !gridMount) return;
if (featureMount) featureMount.innerHTML = '<div style="width:100%;text-align:center;padding:12px;color:#888;">Načítání…</div>';
if (gridMount) gridMount.innerHTML = '';
try {
// Try fast local JSON first
let data = null;
const tryUrls = ['/data/video.json', '/data/videos.json', '/api/videos/latest'];
for (const u of tryUrls){
try {
const res = await fetch(u, {credentials: 'omit', cache: 'no-store'});
if (res.ok) { data = await res.json(); break; }
} catch (_) {}
}
if (!data) throw new Error('No videos data available');
let items = Array.isArray(data.items) ? data.items : data.Items || [];
// ensure most recent first: sort by published_date or published_text desc
const parseDate = (v) => {
const s = v && (v.published_date || v.published_text || '').trim();
// try ISO/date parsing
const t = Date.parse(s);
return isNaN(t) ? 0 : t;
};
items = items.slice().sort((a,b) => parseDate(b) - parseDate(a));
if (featureMount) featureMount.innerHTML = '';
if (!items || items.length === 0){
if (featureMount) featureMount.innerHTML = '<div style="width:100%;text-align:center;padding:12px;color:#888;">Žádná videa zatím nejsou.</div>';
return;
}
const [first, ...rest] = items;
if (featureMount && first) {
const container = document.createElement('div');
container.className = 'items col-xl-12 col-lg-12 col-md-12 col-sm-12 col-ms-12 col-xs-12';
container.appendChild(renderFeatured(first));
featureMount.appendChild(container);
}
if (gridMount && rest.length){
const frag = document.createDocumentFragment();
rest.forEach(v => frag.appendChild(renderGridItem(v)));
gridMount.appendChild(frag);
}
} catch (e) {
console.error('videos load error', e);
if (featureMount) featureMount.innerHTML = '<div style="width:100%;text-align:center;padding:12px;color:#c00;">Nepodařilo se načíst videa.</div>';
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadVideos);
} else {
loadVideos();
}
})();