diff --git a/css/styles.css b/css/styles.css index 388f537..9cdb43f 100644 --- a/css/styles.css +++ b/css/styles.css @@ -1387,6 +1387,130 @@ TELEGRAM BANNER .newsletter-icon { font-size: 2.5rem; margin-bottom: 16px; } .newsletter-hint { color: #65676b; margin-bottom: 16px; font-size: 15px; } +/* ======================================== + NEWSLETTER + CALENDAR LAYOUT + ======================================== */ +.newsletter-calendar-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 32px; + align-items: start; + max-width: 900px; + margin: 0 auto; +} +@media (max-width: 768px) { + .newsletter-calendar-grid { grid-template-columns: 1fr; } +} + +/* Calendar card */ +.calendar-card { + opacity: 0; + transform: scale(0.95); + transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1), + transform 0.6s cubic-bezier(0.16, 1, 0.3, 1); + background: #ffffff; + border: 1px solid #e4e6eb; + border-radius: 12px; + padding: 16px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + max-width: 420px; + margin: 0 auto; + width: 100%; +} +.calendar-card.visible { + opacity: 1 !important; + transform: scale(1) !important; + animation: scaleEntry 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} +.calendar-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} +.calendar-title { + font-size: 1.05rem; + font-weight: 700; + color: #1c1e21; +} +.calendar-nav { + background: #f0f2f5; + border: none; + border-radius: 8px; + width: 32px; + height: 32px; + font-size: 1.1rem; + line-height: 1; + color: #1c1e21; + cursor: pointer; + transition: background 0.2s ease; +} +.calendar-nav:hover { background: #e4e6eb; } +.calendar-weekdays { + display: grid; + grid-template-columns: repeat(7, 1fr); + text-align: center; + font-size: 0.75rem; + font-weight: 600; + color: #65676b; + margin-bottom: 4px; +} +.calendar-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 2px; +} +.calendar-day { + aspect-ratio: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border-radius: 8px; + font-size: 0.85rem; + color: #1c1e21; + position: relative; +} +.calendar-day.empty { visibility: hidden; } +.calendar-day.today { + background: #e7f3ff; + color: #1877f2; + font-weight: 700; +} +.calendar-day.has-event { font-weight: 700; } +.calendar-dots { + display: flex; + gap: 2px; + position: absolute; + bottom: 3px; +} +.calendar-dots i { + width: 5px; + height: 5px; + border-radius: 50%; + background: #1877f2; + display: inline-block; +} +.calendar-events { margin-top: 12px; border-top: 1px solid #e4e6eb; padding-top: 10px; } +.calendar-events-hint { color: #65676b; font-size: 0.85rem; text-align: center; margin: 0; } +.calendar-event-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; } +.calendar-event-list li { + display: flex; + align-items: center; + gap: 10px; + font-size: 0.85rem; + background: #f0f2f5; + border-radius: 8px; + padding: 8px 10px; +} +.calendar-event-date { + font-weight: 700; + color: #1877f2; + white-space: nowrap; +} +.calendar-event-headline { flex: 1; color: #1c1e21; } +.calendar-event-time { color: #65676b; white-space: nowrap; } + /* ======================================== FOOTER ======================================== */ diff --git a/index.html b/index.html index 6b91749..071811f 100644 --- a/index.html +++ b/index.html @@ -395,6 +395,7 @@ diff --git a/js/main.js b/js/main.js index ac942f3..6aca72c 100644 --- a/js/main.js +++ b/js/main.js @@ -17,6 +17,7 @@ document.addEventListener('DOMContentLoaded', () => { currentLang = document.body.dataset.lang || 'de'; checkSession(); loadCommunityStats(); + initCalendar(); // Smooth scroll nav links document.querySelectorAll('.nav-link').forEach(link => { @@ -179,6 +180,7 @@ function initScrollReveal() { '.member-card', '.date-card', '.newsletter-card', + '.calendar-card', '.btn-cta', '.hero-title', '.hero-subtitle' @@ -218,7 +220,7 @@ function initScrollReveal() { // 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'); + 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')); @@ -267,6 +269,7 @@ function setLanguage(lang) { document.querySelectorAll('.lang-btn').forEach(b => { b.classList.toggle('active', b.dataset.lang === lang); }); + renderCalendar(); } function isDE() { @@ -906,21 +909,39 @@ function escapeHtml(text) { // ======================================== // 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(); - - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - renderDates(result.data); + scheduleData = normalizeScheduleResult(result); + + if (scheduleData.length > 0) { + renderDates(scheduleData); } else { document.getElementById('dates-list').innerHTML = '

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

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

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

'; + renderCalendar(); } } @@ -973,6 +994,111 @@ function renderDates(events) { observeRevealCards(container); } +// ======================================== +// CALENDAR (next to newsletter) — month view from schedule webhook +// ======================================== +const MONTH_NAMES_DE = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember']; +const MONTH_NAMES_EN = ['January','February','March','April','May','June','July','August','September','October','November','December']; + +let calendarDate = new Date(); + +function isoDay(d) { + return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); +} + +function eventsByDay() { + const map = {}; + scheduleData.forEach(ev => { + if (!ev || !ev.start) return; + const key = isoDay(new Date(ev.start)); + (map[key] = map[key] || []).push(ev); + }); + return map; +} + +function initCalendar() { + const prev = document.getElementById('calendar-prev'); + const next = document.getElementById('calendar-next'); + if (!prev || !next) return; + prev.addEventListener('click', () => { + calendarDate = new Date(calendarDate.getFullYear(), calendarDate.getMonth() - 1, 1); + renderCalendar(); + }); + next.addEventListener('click', () => { + calendarDate = new Date(calendarDate.getFullYear(), calendarDate.getMonth() + 1, 1); + renderCalendar(); + }); + renderCalendar(); +} + +function renderCalendar() { + const grid = document.getElementById('calendar-grid'); + const title = document.getElementById('calendar-title'); + if (!grid || !title) return; + + const lang = isDE() ? MONTH_NAMES_DE : MONTH_NAMES_EN; + const year = calendarDate.getFullYear(); + const month = calendarDate.getMonth(); + title.innerHTML = '' + escapeHtml(lang[month]) + ' ' + year + ''; + + const firstDay = new Date(year, month, 1); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + // Monday as first weekday: JS getDay() -> 0=Sun..6=Sat + let startOffset = (firstDay.getDay() + 6) % 7; + + const todayKey = isoDay(new Date()); + const eventMap = eventsByDay(); + + let html = ''; + for (let i = 0; i < startOffset; i++) { + html += '
'; + } + for (let day = 1; day <= daysInMonth; day++) { + const key = isoDay(new Date(year, month, day)); + const dayEvents = eventMap[key] || []; + const classes = ['calendar-day']; + if (key === todayKey) classes.push('today'); + if (dayEvents.length > 0) classes.push('has-event'); + let cell = '
'; + cell += '' + day + ''; + if (dayEvents.length > 0) { + cell += '' + ''.repeat(Math.min(dayEvents.length, 3)) + ''; + } + cell += '
'; + html += cell; + } + grid.innerHTML = html; + + // Event list below: upcoming events from this month (or hint) + const eventsEl = document.getElementById('calendar-events'); + if (!eventsEl) return; + const monthEvents = scheduleData + .filter(ev => { + const d = new Date(ev.start); + return d.getFullYear() === year && d.getMonth() === month; + }) + .sort((a, b) => new Date(a.start) - new Date(b.start)); + + if (monthEvents.length === 0) { + eventsEl.innerHTML = '

' + + (isDE() ? 'Keine Termine in diesem Monat.' : 'No events this month.') + '

'; + return; + } + let list = ''; + eventsEl.innerHTML = list; +} + function setCookie(name, value, days) { const expires = new Date(); expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);