Files
paraguay/js/main.js
T

1172 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ========================================
// 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();
loadDates();
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', (e) => {
e.preventDefault();
const url = loginBtn.href;
// Open the link programmatically (reliable than relying on href after DOM changes)
const w = window.open(url, '_blank');
if (!w || w.closed) {
// Popup blocked — fallback: navigate same window
window.location.href = url;
}
// Then swap label and lock
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';
loadUsers();
loadWiki();
} 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 = 'none';
}
}
// ========================================
// 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 = `<span class="rank-badge rank-${rank.level}">${rank.emoji} ${rank.title}</span>`;
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 ? `<a href="mailto:${user.email}">${user.email}</a>` : '-';
const hpEl = document.getElementById('user-detail-homepage');
const hp = user.homepage || '';
hpEl.innerHTML = hp ? `<a href="${hp.startsWith('http') ? '' : 'https://'}${hp}" target="_blank">${hp}</a>` : '-';
document.getElementById('user-detail-location').textContent = user.location || '-';
document.getElementById('user-detail-description').innerHTML = (user.Description || '-').replace(/\n/g, '<br>');
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() {
document.cookie = 'sessionid=; Max-Age=0; path=/';
localStorage.removeItem('paraguay_sessionid');
location.reload();
}
// ========================================
// 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 = '<p class="loading-text">' + (isDE() ? 'Keine Mitglieder gefunden' : 'No members found') + '</p>';
}
} catch (error) {
const container = document.getElementById('members-list');
container.innerHTML = '<p class="loading-text">' + (isDE() ? 'Fehler beim Laden' : 'Error loading') + '</p>';
}
}
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
? `<img src="${user.image}" alt="${user.username}" loading="lazy">`
: `<span class="user-avatar-text">${(user.username || '?').charAt(0).toUpperCase()}</span>`;
card.innerHTML = `
<div class="user-avatar">${avatarContent}</div>
<div class="member-info">
<h4>${user.username || 'N/A'}</h4>
<p class="member-company">${user.company || ''}</p>
${user.location ? `<p class="member-location">📍 ${user.location}</p>` : ''}
${user.posts ? `<p class="member-posts">${user.posts} ${isDE() ? 'Beiträge' : 'posts'}</p>` : ''}
<span class="rank-badge rank-${rank.level}">${rank.emoji} ${rank.title}</span>
</div>
`;
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 = '<p class="loading-text">' + (isDE() ? 'Keine Wiki-Daten' : 'No wiki data') + '</p>';
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 = '<p class="loading-text">' + (isDE() ? 'Fehler beim Laden' : 'Error loading') + '</p>';
}
}
function renderWikiTopics() {
const container = document.getElementById('wiki-topics');
if (!container) return;
const topics = getWikiTopics(wikiData);
let html = '<button class="wiki-topic-filter' + (wikiActiveTopic === '' ? ' active' : '') + '" onclick="filterWikiByTopic(\'\')">' +
(isDE() ? 'Alle' : 'All') + ' <span class="wiki-filter-count">' + wikiData.length + '</span></button>';
topics.forEach(([topic, count]) => {
const esc = escapeHtml(topic).replace(/'/g, "\\'");
html += '<button class="wiki-topic-filter' + (topic === wikiActiveTopic ? ' active' : '') + '" onclick="filterWikiByTopic(\'' + esc + '\')">' +
escapeHtml(topic) + ' <span class="wiki-filter-count">' + count + '</span></button>';
});
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 =
'<button class="wiki-page-btn" onclick="wikiGoPage(' + (wikiCurrentPage - 1) + ')"' + (wikiCurrentPage <= 1 ? ' disabled' : '') + '>' +
'<span class="hide-de">Zurück</span><span class="hide-en">Previous</span></button>' +
'<span class="wiki-page-info">' + wikiCurrentPage + '/' + totalPages + '</span>' +
'<button class="wiki-page-btn" onclick="wikiGoPage(' + (wikiCurrentPage + 1) + ')"' + (wikiCurrentPage >= totalPages ? ' disabled' : '') + '>' +
'<span class="hide-de">Weiter</span><span class="hide-en">Next</span></button>';
}
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 = '<p class="no-results">' + (isDE() ? 'Keine Ergebnisse gefunden' : 'No results found') + '</p>';
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 = '<span class="wiki-topic-badge" style="' + badgeStyle + '">' + escapeHtml(topic).toUpperCase() + '</span>' +
'<h3 class="wiki-subtopic-headline">' + escapeHtml(subtopic) + '</h3>' +
'<p class="wiki-teaser">' + escapeHtml(teaser) + teaserEnd + '</p>' +
'<div class="wiki-card-meta">' +
'<span class="wiki-date">' + dateStr + '</span>' +
'<span class="wiki-read-more">Weiterlesen</span>' +
'</div>';
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 = '<div class="modal wiki-modal"><button class="modal-close" onclick="closeWikiModal()">&times;</button>';
html += '<div class="wiki-modal-header">';
html += '<div class="wiki-modal-topic">' + escapeHtml(topic) + '</div>';
html += '<h2 class="wiki-modal-title">' + escapeHtml(subtopic) + '</h2>';
if (dateStr) html += '<div class="wiki-modal-date">' + dateStr + '</div>';
html += '</div>';
html += '<div class="wiki-modal-text">' + sanitizeHtml(content) + '</div>';
html += '</div>';
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 =
'<p class="no-events">' + (isDE() ? 'Keine Termine geplant' : 'No events scheduled') + '</p>';
}
renderCalendar();
} catch (error) {
document.getElementById('dates-list').innerHTML =
'<p class="no-events">' + (isDE() ? 'Fehler beim Laden' : 'Error loading events') + '</p>';
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 = '<div class="date-header">' +
'<div class="date-icon">📅</div>' +
'<h3 class="date-headline">' + sanitizeHtml(event.headline || 'Event') + '</h3>' +
'</div><div class="date-info">' +
'<div class="date-row">' +
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>' +
'<span>' + dateStr + '</span>' +
'</div><div class="date-row">' +
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>' +
'<span>' + (isDE() ? 'Start' : 'Start') + ' ' + startStr + '</span>' +
'</div><div class="date-row">' +
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>' +
'<span>' + (isDE() ? 'Ende' : 'End') + ' ' + endStr + '</span>' +
'</div></div>';
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 = '<p class="calendar-events-hint">' +
(isDE() ? 'Keine Termine geplant.' : 'No events scheduled.') + '</p>';
return;
}
const sorted = [...scheduleData].sort((a, b) => new Date(a.start) - new Date(b.start));
let list = '<ul class="calendar-event-list">';
sorted.forEach(ev => {
if (!ev || !ev.start) return;
const sd = new Date(ev.start);
const ed = ev.end ? new Date(ev.end) : null;
const dateStr = pad2(sd.getDate()) + '.' + pad2(sd.getMonth() + 1);
const timeStr = pad2(sd.getHours()) + ':' + pad2(sd.getMinutes()) +
(ed ? '' + pad2(ed.getHours()) + ':' + pad2(ed.getMinutes()) : '');
list += '<li><span class="calendar-event-date">' + dateStr + '</span>' +
'<span class="calendar-event-headline">' + sanitizeHtml(ev.headline || 'Event') + '</span>' +
'<span class="calendar-event-time">' + timeStr + '</span></li>';
});
list += '</ul>';
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);
}