// ========================================
// 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';
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 = 'none';
document.getElementById('newsletter-section').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 = `${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.company || ''}
${user.location ? `📍 ${user.location}
` : ''} ${user.posts ? `${user.posts} ${isDE() ? 'Beiträge' : 'posts'}
` : ''} ${rank.emoji} ${rank.title}' + (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() + '' + '' + (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 = '' + (isDE() ? 'Keine Termine geplant.' : 'No events scheduled.') + '
'; return; } const sorted = [...scheduleData].sort((a, b) => new Date(a.start) - new Date(b.start)); let list = '