1072 lines
38 KiB
JavaScript
1072 lines
38 KiB
JavaScript
// ========================================
|
||
// 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();
|
||
}
|
||
|
||
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 = `<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() {
|
||
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 = '<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 = [];
|
||
|
||
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;
|
||
renderWikiCards(wikiData);
|
||
|
||
// Wiki count will be loaded from community webhook
|
||
} else {
|
||
document.getElementById('wiki-grid').innerHTML = '<p class="loading-text">' + (isDE() ? 'Keine Wiki-Daten' : 'No wiki data') + '</p>';
|
||
}
|
||
} catch (error) {
|
||
document.getElementById('wiki-grid').innerHTML = '<p class="loading-text">' + (isDE() ? 'Fehler beim Laden' : 'Error loading') + '</p>';
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
const sorted = [...data].sort((a, b) => new Date(b.updatedAt || b.createdAt) - new Date(a.updatedAt || a.createdAt));
|
||
|
||
sorted.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);
|
||
// Feed topic accent into CSS custom properties (top bar + hover border)
|
||
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 year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
return year + '-' + month + '-' + day;
|
||
}
|
||
|
||
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()">×</button>';
|
||
html += '<div class="wiki-modal-content">';
|
||
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 class="wiki-modal-text">' + escapeHtml(content) + '</div>';
|
||
html += '</div></div>';
|
||
modalOverlay.innerHTML = html;
|
||
modalOverlay.classList.add('active');
|
||
}
|
||
|
||
function searchWiki(term) {
|
||
if (!term.trim()) {
|
||
renderWikiCards(wikiData);
|
||
return;
|
||
}
|
||
|
||
const t = term.toLowerCase();
|
||
const filtered = wikiData.filter(item => {
|
||
const topic = ((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 topic.toLowerCase().includes(t);
|
||
});
|
||
|
||
renderWikiCards(filtered);
|
||
}
|
||
|
||
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: yyyy.mm.dd
|
||
const day = String(startDate.getDate()).padStart(2, '0');
|
||
const month = String(startDate.getMonth() + 1).padStart(2, '0');
|
||
const year = startDate.getFullYear();
|
||
const dateStr = year + '.' + month + '.' + day;
|
||
|
||
// 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) + '.' + sd.getFullYear();
|
||
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);
|
||
}
|