// ======================================== // Paraguay LLC & SRL — Main JS // ======================================== const API_URL = 'https://n8n.odoo4projects.com/webhook/paraguay/login'; const NEWSLETTER_URL = 'https://n8n.odoo4projects.com/webhook/paraguay/newsletter'; const USER_URL = 'https://n8n.odoo4projects.com/webhook/paraguay/user'; const PROFILE_URL = 'https://n8n.odoo4projects.com/webhook/paraguay/profile'; const WIKI_URL = 'https://n8n.odoo4projects.com/webhook/paraguay/wiki'; const COMMUNITY_URL = 'https://n8n.odoo4projects.com/webhook/paraguay/community'; const SCHEDULE_URL = 'https://n8n.odoo4projects.com/webhook/paraguay/schedule'; let currentLang = 'de'; let revealObserver = null; document.addEventListener('DOMContentLoaded', () => { currentLang = document.body.dataset.lang || 'de'; checkSession(); loadCommunityStats(); initCalendar(); // Smooth scroll nav links document.querySelectorAll('.nav-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const target = document.querySelector(link.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth' }); } // Close mobile nav document.getElementById('nav-menu').classList.remove('open'); }); }); // Mobile nav toggle const navToggle = document.getElementById('nav-toggle'); const navMenu = document.getElementById('nav-menu'); if (navToggle) { navToggle.addEventListener('click', () => { navMenu.classList.toggle('open'); }); } // Navbar scroll effect window.addEventListener('scroll', () => { const nav = document.getElementById('main-nav'); if (nav) { nav.classList.toggle('scrolled', window.scrollY > 20); } }); // Language toggle document.querySelectorAll('.lang-btn').forEach(btn => { btn.addEventListener('click', () => { const lang = btn.dataset.lang; setLanguage(lang); // Reload content in new language if (isLoggedIn) { if (document.querySelector('.notebook') || document.getElementById('members-list')) loadUsers(); if (document.getElementById('wiki-grid')) loadWiki(); } }); }); // Member search const memberSearch = document.getElementById('member-search'); if (memberSearch) { memberSearch.addEventListener('input', (e) => { filterMembers(e.target.value); }); } // Wiki search const wikiSearch = document.getElementById('wiki-search'); if (wikiSearch) { wikiSearch.addEventListener('input', (e) => { searchWiki(e.target.value); }); } // Close modals on overlay click document.querySelectorAll('.modal-overlay').forEach(overlay => { overlay.addEventListener('click', (e) => { if (e.target === overlay) { overlay.classList.remove('active'); } }); }); // Close modals on Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { document.querySelectorAll('.modal-overlay.active').forEach(m => m.classList.remove('active')); } }); // Login button — opens Telegram bot deep-link, then locks itself const loginBtn = document.getElementById('login-btn'); if (loginBtn) { loginBtn.addEventListener('click', () => { // No modal: swap label, drop the link, make the button unclickable loginBtn.innerHTML = 'Approve in Telegram'; loginBtn.removeAttribute('href'); loginBtn.classList.add('btn-locked'); }); } // ======================================== // SCROLL-TRIGGERED ANIMATIONS // ======================================== // Fade-in sections on scroll (Intersection Observer) initScrollReveal(); // Ripple effect on buttons initRippleEffects(); // Fade in hero image (prevents blink) initHeroImageFade(); }); // ======================================== // HERO IMAGE FADE IN — prevents blink // ======================================== function initHeroImageFade() { const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const heroImage = document.querySelector('.hero-image'); if (!heroImage) return; if (prefersReducedMotion) { heroImage.classList.add('loaded'); return; } if (heroImage.complete && heroImage.naturalHeight > 0) { requestAnimationFrame(() => heroImage.classList.add('loaded')); return; } heroImage.addEventListener('load', () => requestAnimationFrame(() => heroImage.classList.add('loaded')), { once: true }); heroImage.addEventListener('error', () => requestAnimationFrame(() => heroImage.classList.add('loaded')), { once: true }); } // ======================================== // SCROLL REVEAL (Intersection Observer) // ======================================== function initScrollReveal() { // Respect reduced motion preference const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (prefersReducedMotion) return; const observerOptions = { root: null, rootMargin: '0px 0px -60px 0px', threshold: 0.1 }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('visible'); observer.unobserve(entry.target); // Only animate once } }); }, observerOptions); // Keep a handle so dynamically rendered cards (members, wiki, dates) can be observed too revealObserver = observer; // Observe all sections for reveal animation document.querySelectorAll('.section, .telegram-banner, #newsletter-section').forEach(section => { section.style.opacity = '0'; section.style.transform = 'translateY(20px)'; section.style.transition = 'opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1), transform 0.6s cubic-bezier(0.16, 1, 0.3, 1)'; observer.observe(section); }); // Observe individual interactive elements for staggered reveals const staggerElements = [ '.section-title', '.stat', '.about-card', '.wiki-card-new', '.member-card', '.date-card', '.newsletter-card', '.calendar-card', '.btn-cta', '.hero-title', '.hero-subtitle' ]; staggerElements.forEach(selector => { document.querySelectorAll(selector).forEach(el => { el.style.opacity = '0'; el.style.transition = 'opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1), transform 0.6s cubic-bezier(0.16, 1, 0.3, 1)'; observer.observe(el); }); }); // Hero elements fade in on load (not scroll) if (window.location.hash === '' || window.location.hash === '#hero') { document.querySelectorAll('.hero-title, .hero-subtitle, .btn-cta').forEach((el, i) => { setTimeout(() => { el.classList.add('visible'); el.style.opacity = ''; el.style.transform = ''; }, 300 + i * 150); }); // Also observe hero stats document.querySelectorAll('.stat').forEach((el, i) => { setTimeout(() => { el.classList.add('visible'); el.style.opacity = ''; el.style.transform = ''; }, 400 + i * 100); }); } } // ======================================== // REVEAL OBSERVER FOR DYNAMICALLY RENDERED CARDS // Cards loaded via webhooks (members, wiki, dates) are created AFTER // initScrollReveal() ran, so they were never observed -> stuck at opacity 0. // ======================================== function observeRevealCards(container) { const els = (container || document).querySelectorAll('.wiki-card-new, .member-card, .date-card, .newsletter-card, .calendar-card'); if (!revealObserver) { // Reduced-motion (observer never created) or init missed: show immediately els.forEach(el => el.classList.add('visible')); return; } els.forEach(el => revealObserver.observe(el)); } // ======================================== // RIPPLE EFFECT ON BUTTONS // ======================================== function initRippleEffects() { document.querySelectorAll('.btn, .lang-btn').forEach(btn => { btn.addEventListener('click', function(e) { const rect = this.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const ripple = document.createElement('span'); ripple.style.cssText = ` position: absolute; border-radius: 50%; background: rgba(255, 255, 255, 0.4); width: 0; height: 0; left: ${x}px; top: ${y}px; transform: translate(-50%, -50%); pointer-events: none; animation: rippleExpand 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards; `; this.style.position = this.style.position || 'relative'; this.style.overflow = 'hidden'; this.appendChild(ripple); setTimeout(() => ripple.remove(), 600); }); }); } // ======================================== // LANGUAGE // ======================================== function setLanguage(lang) { currentLang = lang; document.body.dataset.lang = lang; document.querySelectorAll('.lang-btn').forEach(b => { b.classList.toggle('active', b.dataset.lang === lang); }); renderCalendar(); if (wikiData.length) { renderWikiTopics(); renderWikiPage(); } } function isDE() { return currentLang !== 'en'; } // ======================================== // COMMUNITY STATS // ======================================== async function loadCommunityStats() { try { const response = await fetch(COMMUNITY_URL); if (!response.ok) throw new Error('Failed to fetch'); const json = await response.json(); // API returns an array: [{"users": 4, "topics": 27, "posts": 58}] const d = Array.isArray(json) ? json[0] : json; if (d.users !== undefined) { animateCounter('stat-members', d.users); } if (d.posts !== undefined) { animateCounter('stat-posts', d.posts); } if (d.topics !== undefined) { animateCounter('stat-wiki', d.topics); } } catch (error) { console.error('Failed to load community stats:', error); } } function animateCounter(elementId, targetNumber) { const element = document.getElementById(elementId); if (!element) return; let current = 0; const duration = 1500; // ms const steps = 40; const increment = targetNumber / steps; const stepTime = duration / steps; const timer = setInterval(() => { current += increment; if (current >= targetNumber) { element.textContent = targetNumber; clearInterval(timer); } else { element.textContent = Math.floor(current); } }, stepTime); } // ======================================== // AUTH / SESSION // ======================================== let isLoggedIn = false; function isSessionValid() { const sid = getCookie('sessionid'); return sid.length > 0; } function checkSession() { isLoggedIn = isSessionValid(); if (isLoggedIn) { document.body.dataset.loggedIn = 'true'; document.getElementById('login-btn').style.display = 'none'; document.getElementById('logout-btn').style.display = 'inline-flex'; document.getElementById('profile-btn').style.display = 'inline-flex'; document.getElementById('telegram-banner').style.display = 'none'; document.getElementById('main-content').style.display = 'block'; document.getElementById('newsletter-section').style.display = 'block'; // Remove locked overlays document.querySelectorAll('.section-locked').forEach(el => el.style.display = 'none'); loadUsers(); loadWiki(); loadDates(); } else { document.body.dataset.loggedIn = 'false'; document.getElementById('login-btn').style.display = 'inline-flex'; document.getElementById('logout-btn').style.display = 'none'; document.getElementById('profile-btn').style.display = 'none'; document.getElementById('telegram-banner').style.display = 'block'; document.getElementById('main-content').style.display = 'block'; document.getElementById('newsletter-section').style.display = 'none'; // Show locked overlays document.querySelectorAll('.section-locked').forEach(el => el.style.display = ''); } } // ======================================== // LOGIN MODAL // ======================================== function openLoginModal() { document.getElementById('login-modal-overlay').classList.add('active'); // Reset to step 1 document.querySelectorAll('.login-step').forEach(s => s.classList.remove('active')); document.getElementById('login-step-1').classList.add('active'); document.getElementById('telegram-user').value = ''; document.getElementById('verification-code').value = ''; } function closeLoginModal() { document.getElementById('login-modal-overlay').classList.remove('active'); } // ======================================== // PROFILE MODAL // ======================================== function openProfileModal() { const sid = getCookie('sessionid'); if (!sid) { alert('Bitte anmelden.'); return; } fetch(PROFILE_URL + '?sessionid=' + encodeURIComponent(sid)) .then(r => r.json()) .then(user => { document.getElementById('profile-username-display').textContent = user.username || ''; const rank = getRank(user.posts || 0); const badge = document.getElementById('profile-rank-badge'); badge.innerHTML = `${rank.emoji} ${rank.title}`; document.getElementById('profile-company').value = user.company || ''; document.getElementById('profile-email').value = user.email || ''; document.getElementById('profile-homepage').value = user.homepage || ''; document.getElementById('profile-image').value = user.image || ''; document.getElementById('profile-location').value = user.location || ''; document.getElementById('profile-description').value = user.Description || ''; document.getElementById('profile-publish').checked = true; document.getElementById('profile-modal-overlay').classList.add('active'); }) .catch(() => alert('Fehler beim Laden des Profils.')); } function closeProfileModal() { document.getElementById('profile-modal-overlay').classList.remove('active'); } function saveProfile() { const sid = getCookie('sessionid'); fetch(PROFILE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: document.getElementById('profile-username-display').textContent, company: document.getElementById('profile-company').value.trim(), email: document.getElementById('profile-email').value, homepage: document.getElementById('profile-homepage').value, image: document.getElementById('profile-image').value, location: document.getElementById('profile-location').value, Description: document.getElementById('profile-description').value, sessionid: sid, publish: document.getElementById('profile-publish').checked }) }) .then(r => r.ok ? alert('Profil gespeichert!') : alert('Fehler beim Speichern.')) .catch(() => alert('Fehler bei der Verbindung.')); } // ======================================== // USER DETAIL MODAL // ======================================== function openUserDetailModal(user) { const avatar = document.getElementById('user-detail-avatar'); const avatarImg = avatar.querySelector('img'); const avatarInitial = document.getElementById('user-detail-initial'); if (user.image) { if (avatarImg) { avatarImg.src = user.image; avatarImg.alt = user.username; avatarImg.style.display = ''; } else { const img = document.createElement('img'); img.src = user.image; img.alt = user.username; img.style.display = ''; avatar.appendChild(img); } avatarInitial.textContent = ''; } else { if (avatarImg) avatarImg.style.display = 'none'; avatarInitial.textContent = (user.username || '?').charAt(0).toUpperCase(); } document.getElementById('user-detail-username').textContent = '@' + (user.username || 'Unknown'); const rank = getRank(user.posts || 0); document.getElementById('user-detail-rank-badge').className = `rank-badge rank-${rank.level}`; document.getElementById('user-detail-rank-badge').textContent = rank.emoji + ' ' + rank.title; document.getElementById('user-detail-company').textContent = user.company || ''; const emailEl = document.getElementById('user-detail-email'); emailEl.innerHTML = user.email ? `${user.email}` : '-'; const hpEl = document.getElementById('user-detail-homepage'); const hp = user.homepage || ''; hpEl.innerHTML = hp ? `${hp}` : '-'; document.getElementById('user-detail-location').textContent = user.location || '-'; document.getElementById('user-detail-description').innerHTML = (user.Description || '-').replace(/\n/g, '
'); document.getElementById('user-detail-modal-overlay').classList.add('active'); } function closeUserDetailModal() { document.getElementById('user-detail-modal-overlay').classList.remove('active'); } // ======================================== // LOGIN FLOW // ======================================== function sendTelegramUser() { const user = document.getElementById('telegram-user').value.trim(); if (!user) { alert('Bitte geben Sie einen Telegram-Nutzernamen ein.'); return; } const btn = event.target; btn.disabled = true; btn.textContent = isDE() ? 'Sende...' : 'Sending...'; fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: user }) }) .then(r => r.json()) .then(data => { if (data.sessionid) { setCookie('sessionid', data.sessionid, 1); document.getElementById('login-step-1').classList.remove('active'); document.getElementById('login-step-3').classList.add('active'); } else if (data.status === 'code send') { document.getElementById('login-step-1').classList.remove('active'); document.getElementById('login-step-2').classList.add('active'); } else { alert(isDE() ? 'Benutzername nicht gefunden. Bitte über Telegram Bot anmelden.' : 'Username not found. Please sign up via Telegram bot.'); } }) .catch(() => { btn.disabled = false; alert(isDE() ? 'Verbindungsfehler' : 'Connection error'); }); } function verifyCode() { const code = document.getElementById('verification-code').value.trim(); if (!code) { alert('Bitte geben Sie den Code ein.'); return; } fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: document.getElementById('telegram-user').value, code: code, sessionid: getCookie('sessionid') }) }) .then(r => r.json()) .then(data => { if (data.sessionid) { setCookie('sessionid', data.sessionid, 1); checkSession(); closeLoginModal(); } else { alert(isDE() ? 'Ungültiger Code' : 'Invalid code'); } }) .catch(() => alert(isDE() ? 'Verbindungsfehler' : 'Connection error')); } function submitLoginData() { const name = document.getElementById('full-name').value.trim(); const llc = document.getElementById('llc-name').value.trim(); if (!name || !llc) { alert(isDE() ? 'Bitte Name und LLC eintragen' : 'Please enter name and LLC'); return; } fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: document.getElementById('telegram-user').value, name: name, llc: llc }) }) .then(r => r.json()) .then(data => { if (data.sessionid) { setCookie('sessionid', data.sessionid, 1); checkSession(); closeLoginModal(); } else { alert(isDE() ? 'Registrierung fehlgeschlagen' : 'Registration failed'); } }) .catch(() => alert(isDE() ? 'Verbindungsfehler' : 'Connection error')); } function logout() { setCookie('sessionid', '', -1); checkSession(); } // ======================================== // NEWSLETTER // ======================================== function subscribeNewsletter() { const email = document.getElementById('newsletter-email').value.trim(); if (!email) { alert(isDE() ? 'Bitte E-Mail eingeben' : 'Please enter email'); return; } fetch(NEWSLETTER_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email }) }) .then(r => { if (r.ok) { document.getElementById('newsletter-form').classList.add('hidden'); document.getElementById('newsletter-success').classList.remove('hidden'); } else { alert(isDE() ? 'Fehler beim Abonnieren' : 'Error subscribing'); } }) .catch(() => alert(isDE() ? 'Verbindungsfehler' : 'Connection error')); } // ======================================== // USERS / MEMBERS // ======================================== async function loadUsers() { try { const response = await fetch(USER_URL); if (!response.ok) throw new Error('Failed to fetch'); const data = await response.json(); const container = document.getElementById('members-list'); if (data.data && Array.isArray(data.data)) { container.innerHTML = ''; container.style.display = 'grid'; data.data.forEach(user => { const card = createMemberCard(user); container.appendChild(card); }); observeRevealCards(container); // Update stats will be handled by loadCommunityStats } else { container.innerHTML = '

' + (isDE() ? 'Keine Mitglieder gefunden' : 'No members found') + '

'; } } catch (error) { const container = document.getElementById('members-list'); container.innerHTML = '

' + (isDE() ? 'Fehler beim Laden' : 'Error loading') + '

'; } } function createMemberCard(user) { const card = document.createElement('div'); card.className = 'member-card'; card.setAttribute('role', 'button'); card.setAttribute('tabindex', '0'); const rank = getRank(user.posts || 0); const avatarContent = user.image ? `${user.username}` : `${(user.username || '?').charAt(0).toUpperCase()}`; card.innerHTML = `
${avatarContent}

${user.username || 'N/A'}

${user.company || ''}

${user.location ? `

📍 ${user.location}

` : ''} ${user.posts ? `

${user.posts} ${isDE() ? 'Beiträge' : 'posts'}

` : ''} ${rank.emoji} ${rank.title}
`; card.addEventListener('click', (e) => { e.stopPropagation(); openUserDetailModal(user); }); card.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openUserDetailModal(user); } }); return card; } function filterMembers(term) { const cards = document.querySelectorAll('.member-card'); const t = term.toLowerCase(); cards.forEach(card => { const text = card.textContent.toLowerCase(); card.style.display = text.includes(t) ? 'flex' : 'none'; }); } // ======================================== // RANKS // ======================================== function getRank(posts) { if (posts >= 500) return { level: 'legend', title: isDE() ? 'Legende' : 'Legend', emoji: '👑' }; if (posts >= 200) return { level: 'expert', title: isDE() ? 'Experte' : 'Expert', emoji: '🏆' }; if (posts >= 100) return { level: 'veteran', title: isDE() ? 'Veteran' : 'Veteran', emoji: '💎' }; if (posts >= 50) return { level: 'contributor', title: isDE() ? 'Mitwirkender' : 'Contributor', emoji: '⭐' }; if (posts >= 30) return { level: 'member', title: isDE() ? 'Mitglied' : 'Member', emoji: '🌳' }; if (posts >= 5) return { level: 'beginner', title: isDE() ? 'Einsteiger' : 'Beginner', emoji: '🌿' }; return { level: 'rookie', title: isDE() ? 'Anfänger' : 'Rookie', emoji: '🌱' }; } // ======================================== // TOPIC COLORS — each topic gets a distinct color // ======================================== const TOPIC_COLORS = { '🌎 Internationales Geschäft': { bg: '#E0F2F1', text: '#00695C', border: '#00695C' }, '🏦 Banken & Finanzen Filialen': { bg: '#E3F2FD', text: '#1565C0', border: '#1565C0' }, '🏦 Banken & Finanzen allgmein': { bg: '#E8EAF6', text: '#283593', border: '#283593' }, '💰 Steuern': { bg: '#FFF3E0', text: '#E65100', border: '#E65100' }, '💻 Software & Digital Operations': { bg: '#EDE7F6', text: '#4527A0', border: '#4527A0' }, '📑 Buchhaltung': { bg: '#E8F5E9', text: '#2E7D32', border: '#2E7D32' } }; function getTopicColor(topic) { // Try exact match first if (TOPIC_COLORS[topic]) return TOPIC_COLORS[topic]; // Try matching without emoji const cleanTopic = topic.replace(/^[^\s]+\s/, ''); for (const key of Object.keys(TOPIC_COLORS)) { const cleanKey = key.replace(/^[^\s]+\s/, ''); if (cleanTopic === cleanKey) return TOPIC_COLORS[key]; } // Fallback: generate color from topic string let hash = 0; for (let i = 0; i < topic.length; i++) { hash = topic.charCodeAt(i) + ((hash << 5) - hash); } const hues = [200, 180, 240, 30, 140, 60]; // blue, teal, purple, orange, green, amber const idx = Math.abs(hash) % hues.length; const color = hues[idx]; return { bg: `hsl(${color}, 70%, 92%)`, text: `hsl(${color}, 60%, 35%)`, border: `hsl(${color}, 60%, 35%)` }; } // ======================================== // WIKI // ======================================== // ======================================== let wikiData = []; let wikiCurrentPage = 1; let wikiPerPage = 9; let wikiFilteredData = []; let wikiActiveTopic = ''; // Get unique topics with counts from the full wiki data function getWikiTopics(data) { const map = {}; data.forEach(item => { const t = currentLang === 'en' && item.topic_en ? item.topic_en : (item.topic || 'Unknown'); map[t] = (map[t] || 0) + 1; }); return Object.entries(map).sort(); } async function loadWiki() { try { const response = await fetch(WIKI_URL); if (!response.ok) throw new Error('Failed to fetch'); const result = await response.json(); if (result.data && Array.isArray(result.data)) { wikiData = result.data; wikiFilteredData = [...result.data].sort((a, b) => new Date(b.updatedAt || b.createdAt) - new Date(a.updatedAt || a.createdAt)); wikiCurrentPage = 1; wikiActiveTopic = ''; renderWikiTopics(); renderWikiPage(); } else { document.getElementById('wiki-grid').innerHTML = '

' + (isDE() ? 'Keine Wiki-Daten' : 'No wiki data') + '

'; const c = document.getElementById('wiki-counter'); if (c) c.innerHTML = ''; const p = document.getElementById('wiki-pagination'); if (p) p.innerHTML = ''; } } catch (error) { document.getElementById('wiki-grid').innerHTML = '

' + (isDE() ? 'Fehler beim Laden' : 'Error loading') + '

'; } } function renderWikiTopics() { const container = document.getElementById('wiki-topics'); if (!container) return; const topics = getWikiTopics(wikiData); let html = ''; topics.forEach(([topic, count]) => { const esc = escapeHtml(topic).replace(/'/g, "\\'"); html += ''; }); container.innerHTML = html; } function filterWikiByTopic(topic) { wikiActiveTopic = topic; if (!topic) { wikiFilteredData = [...wikiData].sort((a, b) => new Date(b.updatedAt || b.createdAt) - new Date(a.updatedAt || a.createdAt)); } else { wikiFilteredData = [...wikiData].filter(item => { const t = currentLang === 'en' && item.topic_en ? item.topic_en : (item.topic || ''); return t === topic; }).sort((a, b) => new Date(b.updatedAt || b.createdAt) - new Date(a.updatedAt || a.createdAt)); } wikiCurrentPage = 1; renderWikiTopics(); renderWikiPage(); } function renderWikiPage() { const total = wikiFilteredData.length; const totalPages = Math.max(1, Math.ceil(total / wikiPerPage)); if (wikiCurrentPage > totalPages) wikiCurrentPage = totalPages; const start = (wikiCurrentPage - 1) * wikiPerPage; const pageData = wikiFilteredData.slice(start, start + wikiPerPage); renderWikiCards(pageData); renderWikiCounter(start, total); renderWikiPagination(totalPages); } function renderWikiCounter(start, total) { const el = document.getElementById('wiki-counter'); if (!el) return; if (total === 0 || start >= total) { el.innerHTML = ''; return; } const shown = Math.min(wikiPerPage, total - start); el.textContent = start + shown + '/' + total; } function renderWikiPagination(totalPages) { const el = document.getElementById('wiki-pagination'); if (!el) return; if (totalPages <= 1) { el.innerHTML = ''; return; } el.innerHTML = '' + '' + wikiCurrentPage + '/' + totalPages + '' + ''; } function wikiGoPage(page) { const total = wikiFilteredData.length; const totalPages = Math.max(1, Math.ceil(total / wikiPerPage)); if (page < 1 || page > totalPages) return; wikiCurrentPage = page; const start = (wikiCurrentPage - 1) * wikiPerPage; renderWikiCards(wikiFilteredData.slice(start, start + wikiPerPage)); renderWikiCounter(start, total); renderWikiPagination(totalPages); const section = document.querySelector('#wiki'); if (section) section.scrollIntoView({ behavior: 'smooth', block: 'start' }); } function renderWikiCards(data) { const grid = document.getElementById('wiki-grid'); grid.innerHTML = ''; if (data.length === 0) { grid.innerHTML = '

' + (isDE() ? 'Keine Ergebnisse gefunden' : 'No results found') + '

'; return; } data.forEach((item, idx) => { const card = document.createElement('div'); card.className = 'wiki-card-new stagger-' + (idx % 4 + 1); card.setAttribute('role', 'button'); card.setAttribute('tabindex', '0'); const topic = currentLang === 'en' && item.topic_en ? item.topic_en : (item.topic || 'Unknown'); const topicColor = getTopicColor(topic); card.style.setProperty('--topic-accent', topicColor.border); const subtopic = currentLang === 'en' && item.subtopic_en ? item.subtopic_en : (item.subtopic || ''); const content = currentLang === 'en' && item.wiki_en ? item.wiki_en : (item.wiki || ''); const dateStr = formatDate(new Date(item.updatedAt || item.createdAt)); const teaser = content.trim().substring(0, 180); const teaserEnd = content.trim().length > 180 ? '...' : ''; const badgeStyle = 'background:' + topicColor.bg + ';color:' + topicColor.text; card.innerHTML = '' + escapeHtml(topic).toUpperCase() + '' + '

' + escapeHtml(subtopic) + '

' + '

' + escapeHtml(teaser) + teaserEnd + '

' + '
' + '' + dateStr + '' + 'Weiterlesen' + '
'; card.addEventListener('click', () => openWikiModalSingle(item)); card.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openWikiModalSingle(item); } }); grid.appendChild(card); }); observeRevealCards(grid); } function formatDate(date) { if (!(date instanceof Date) || isNaN(date)) return ''; const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return day + '.' + month; } function closeWikiModal() { const overlay = document.getElementById('wiki-modal-overlay'); if (overlay) overlay.classList.remove('active'); } function openWikiModalSingle(item) { let modalOverlay = document.getElementById('wiki-modal-overlay'); if (!modalOverlay) { modalOverlay = document.createElement('div'); modalOverlay.id = 'wiki-modal-overlay'; modalOverlay.className = 'modal-overlay'; document.body.appendChild(modalOverlay); } const topic = currentLang === 'en' && item.topic_en ? item.topic_en : (item.topic || 'Unknown'); const subtopic = currentLang === 'en' && item.subtopic_en ? item.subtopic_en : (item.subtopic || ''); const content = currentLang === 'en' && item.wiki_en ? item.wiki_en : (item.wiki || ''); const dateStr = formatDate(new Date(item.updatedAt || item.createdAt)); let html = ''; modalOverlay.innerHTML = html; modalOverlay.classList.add('active'); } function searchWiki(term) { const t = term.toLowerCase().trim(); if (!t) { wikiActiveTopic = ''; wikiFilteredData = [...wikiData].sort((a, b) => new Date(b.updatedAt || b.createdAt) - new Date(a.updatedAt || a.createdAt)); } else { wikiActiveTopic = ''; wikiFilteredData = wikiData.filter(item => { const text = ((item.topic_en && currentLang === 'en') ? item.topic_en : (item.topic || '')) + ' ' + ((item.subtopic_en && currentLang === 'en') ? item.subtopic_en : (item.subtopic || '')) + ' ' + ((item.wiki_en && currentLang === 'en') ? item.wiki_en : (item.wiki || '')); return text.toLowerCase().includes(t); }).sort((a, b) => new Date(b.updatedAt || b.createdAt) - new Date(a.updatedAt || a.createdAt)); } renderWikiTopics(); wikiCurrentPage = 1; renderWikiPage(); } function getTopicColor(topic) { const TOPIC_COLORS = { '🌎 Internationales Geschäft': { bg: '#E0F2F1', text: '#00695C', border: '#00695C' }, '🏦 Banken & Finanzen Filialen': { bg: '#E3F2FD', text: '#1565C0', border: '#1565C0' }, '🏦 Banken & Finanzen allgmein': { bg: '#E8EAF6', text: '#283593', border: '#283593' }, '💰 Steuern': { bg: '#FFF3E0', text: '#E65100', border: '#E65100' }, '💻 Software & Digital Operations': { bg: '#EDE7F6', text: '#4527A0', border: '#4527A0' }, '📑 Buchhaltung': { bg: '#E8F5E9', text: '#2E7D32', border: '#2E7D32' } }; if (TOPIC_COLORS[topic]) return TOPIC_COLORS[topic]; const cleanTopic = topic.replace(/^[^\s]+\s/, ''); for (const key of Object.keys(TOPIC_COLORS)) { const cleanKey = key.replace(/^[^\s]+\s/, ''); if (cleanTopic === cleanKey) return TOPIC_COLORS[key]; } let hash = 0; for (let i = 0; i < topic.length; i++) { hash = topic.charCodeAt(i) + ((hash << 5) - hash); } const hues = [200, 180, 240, 30, 140, 60]; const idx = Math.abs(hash) % hues.length; const color = hues[idx]; return { bg: 'hsl(' + color + ', 70%, 92%)', text: 'hsl(' + color + ', 60%, 35%)', border: 'hsl(' + color + ', 60%, 35%)' }; } // ======================================== // COOKIES // ======================================== function getCookie(name) { const localVal = localStorage.getItem('paraguay_' + name); if (localVal) return localVal; const value = '; ' + document.cookie; const parts = value.split('; ' + name + '='); if (parts.length === 2) { return parts.pop().split(';').shift().replace(/^"|"$/g, ''); } return ''; } // ======================================== // ESCAPE HTML — utility function // ======================================== function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ======================================== // DATES / EVENTS // ======================================== let scheduleData = []; // Schedule webhook may return { data: [...] } or [{ data: [...] }] — normalize function normalizeScheduleResult(result) { if (!result) return []; if (Array.isArray(result)) { for (const item of result) { if (item && Array.isArray(item.data)) return item.data; } return []; } if (Array.isArray(result.data)) return result.data; return []; } async function loadDates() { try { const response = await fetch(SCHEDULE_URL); if (!response.ok) throw new Error('Failed to fetch'); const result = await response.json(); scheduleData = normalizeScheduleResult(result); if (scheduleData.length > 0) { renderDates(scheduleData); } else { document.getElementById('dates-list').innerHTML = '

' + (isDE() ? 'Keine Termine geplant' : 'No events scheduled') + '

'; } renderCalendar(); } catch (error) { document.getElementById('dates-list').innerHTML = '

' + (isDE() ? 'Fehler beim Laden' : 'Error loading events') + '

'; renderCalendar(); } } function renderDates(events) { const container = document.getElementById('dates-list'); container.innerHTML = ''; // Sort events by date const sorted = events.sort((a, b) => new Date(a.start) - new Date(b.start)); sorted.forEach((event, idx) => { const card = document.createElement('div'); card.className = 'date-card stagger-' + (idx % 4 + 1); // Parse date/time from ISO string const startDate = new Date(event.start); const endDate = new Date(event.end); // Format date: dd.mm const day = String(startDate.getDate()).padStart(2, '0'); const month = String(startDate.getMonth() + 1).padStart(2, '0'); const dateStr = day + '.' + month; // Format time: HH:MM (local time) const startHour = String(startDate.getHours()).padStart(2, '0'); const startMinute = String(startDate.getMinutes()).padStart(2, '0'); const endHour = String(endDate.getHours()).padStart(2, '0'); const endMinute = String(endDate.getMinutes()).padStart(2, '0'); const startStr = startHour + ':' + startMinute; const endStr = endHour + ':' + endMinute; card.innerHTML = '
' + '
📅
' + '

' + sanitizeHtml(event.headline || 'Event') + '

' + '
' + '
' + '' + '' + dateStr + '' + '
' + '' + '' + (isDE() ? 'Start' : 'Start') + ' ' + startStr + '' + '
' + '' + '' + (isDE() ? 'Ende' : 'End') + ' ' + endStr + '' + '
'; container.appendChild(card); }); observeRevealCards(container); } // ======================================== // CALENDAR (next to newsletter) — plain event list from schedule webhook // ======================================== function pad2(n) { return String(n).padStart(2, '0'); } // Webhook text may contain simple HTML (e.g. links). Sanitize before injecting: // strip script/iframe/... tags, on* handlers and javascript: URLs; force // links to open in a new tab. function sanitizeHtml(html) { const div = document.createElement('div'); div.innerHTML = String(html || ''); div.querySelectorAll('script, style, iframe, object, embed, link, meta').forEach(n => n.remove()); const walk = (node) => { Array.from(node.childNodes || []).forEach(child => { if (child.nodeType !== 1) return; Array.from(child.attributes || []).forEach(attr => { const name = attr.name.toLowerCase(); const val = (attr.value || '').replace(/\s+/g, '').toLowerCase(); if (name.startsWith('on') || ((name === 'href' || name === 'src') && val.startsWith('javascript:'))) { child.removeAttribute(attr.name); } }); if (child.tagName === 'A') { child.setAttribute('target', '_blank'); child.setAttribute('rel', 'noopener'); } walk(child); }); }; walk(div); return div.innerHTML; } function initCalendar() { renderCalendar(); } function renderCalendar() { const eventsEl = document.getElementById('calendar-events'); if (!eventsEl) return; if (scheduleData.length === 0) { eventsEl.innerHTML = '

' + (isDE() ? 'Keine Termine geplant.' : 'No events scheduled.') + '

'; return; } const sorted = [...scheduleData].sort((a, b) => new Date(a.start) - new Date(b.start)); let list = ''; eventsEl.innerHTML = list; } function setCookie(name, value, days) { const expires = new Date(); expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000); document.cookie = name + '=' + encodeURIComponent(value) + ';expires=' + expires.toUTCString() + ';path=/'; localStorage.setItem('paraguay_' + name, value); }