with '#' will be ignored, and an empty message aborts the commit. On branch main Initial commit Changes to be committed: new file: .DS_Store new file: .env new file: .gitignore new file: ai-worker/Dockerfile new file: ai-worker/requirements.txt new file: ai-worker/worker.py new file: background-worker/Dockerfile new file: background-worker/go.mod new file: background-worker/go.sum new file: background-worker/main.go new file: background-worker/market.go new file: background-worker/rmv.go new file: background-worker/rss.go new file: background-worker/sql_work.go new file: db/Dockerfile new file: db/init.sql new file: docker-compose.yml new file: server-app/dockerfile new file: server-app/go.mod new file: server-app/go.sum new file: server-app/main.go new file: volumes/.DS_Store new file: volumes/db-init/.DS_Store new file: volumes/db-init/data/news_rss_feeds.csv new file: volumes/web/.DS_Store new file: volumes/web/static/css/blog.css new file: volumes/web/static/css/index-lite.css new file: volumes/web/static/css/index.css new file: volumes/web/static/css/mandelbrot.css new file: volumes/web/static/img/minecraft.png new file: volumes/web/static/js/blog.js new file: volumes/web/static/js/index-lite.js new file: volumes/web/static/js/index.js new file: volumes/web/static/js/mandelbrot.js new file: volumes/web/static/media/cantina.mp3 new file: volumes/web/static/media/countdowns.json new file: volumes/web/static/media/gong.mp4 new file: volumes/web/template/blog.html new file: volumes/web/template/index-lite.html new file: volumes/web/template/index.html new file: volumes/web/template/mandelbrot.html
189 lines
5.1 KiB
JavaScript
189 lines
5.1 KiB
JavaScript
let timersData = [];
|
|
let detailOpenFor = null;
|
|
|
|
function formatDiff(target) {
|
|
const targetDate = target instanceof Date ? target : new Date(target);
|
|
const now = new Date();
|
|
const diff = targetDate - now;
|
|
|
|
if (diff <= 0) return "Abgelaufen";
|
|
|
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
const hours = Math.floor((diff / (1000 * 60 * 60)) % 24);
|
|
const minutes = Math.floor((diff / (1000 * 60)) % 60);
|
|
const seconds = Math.floor((diff / 1000) % 60);
|
|
|
|
return `${days}d ${hours}h ${minutes}m ${seconds}s`;
|
|
}
|
|
|
|
fetch("/api/countdowns")
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
timersData = data.timers || [];
|
|
|
|
const mainTargetDate = new Date(data.main.target);
|
|
const mainLabel = data.main.label || "";
|
|
const mainLabelEl = document.getElementById("main-label");
|
|
const mainCountdownEl = document.getElementById("main-countdown");
|
|
|
|
function updateMainTimer() {
|
|
const now = new Date();
|
|
const diff = mainTargetDate - now;
|
|
|
|
if (diff <= 0) {
|
|
mainLabelEl.textContent = mainLabel;
|
|
mainCountdownEl.textContent = "Abgelaufen";
|
|
return;
|
|
}
|
|
|
|
mainLabelEl.textContent = mainLabel;
|
|
mainCountdownEl.textContent = formatDiff(mainTargetDate);
|
|
}
|
|
|
|
function updateSmallTimers() {
|
|
timersData.forEach(t => {
|
|
const el = document.getElementById(t.id);
|
|
if (!el) return;
|
|
|
|
const now = new Date();
|
|
const nextEntry = t.targets
|
|
.map(e => ({ ...e, date: new Date(e.time) }))
|
|
.find(e => e.date > now);
|
|
|
|
if (nextEntry) {
|
|
el.textContent = `${nextEntry.label}\n${formatDiff(nextEntry.date)}`;
|
|
} else {
|
|
el.textContent = `${t.label}\nAbgelaufen`;
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateDetailsPanel() {
|
|
if (!detailOpenFor) return;
|
|
const timer = timersData.find(t => t.id === detailOpenFor);
|
|
if (!timer) return;
|
|
|
|
timer.targets.forEach((target, idx) => {
|
|
const el = document.getElementById(`detail-${timer.id}-${idx}`);
|
|
if (!el) return;
|
|
el.textContent = formatDiff(target.time);
|
|
});
|
|
}
|
|
|
|
function renderDetails(timer) {
|
|
const detailEl = document.getElementById("timer-details");
|
|
if (!detailEl) return;
|
|
|
|
detailOpenFor = timer.id;
|
|
const entries = timer.targets.map((target, idx) => {
|
|
const entryId = `detail-${timer.id}-${idx}`;
|
|
return `
|
|
<div class="timer-entry">
|
|
<div class="label">${target.label}</div>
|
|
<div class="countdown" id="${entryId}"></div>
|
|
</div>
|
|
`;
|
|
}).join("");
|
|
|
|
detailEl.innerHTML = `
|
|
<button class="close-btn" id="timer-details-close">Schließen</button>
|
|
<h3>${timer.label}</h3>
|
|
<div class="timer-meta">${timer.targets.length} Termine</div>
|
|
${entries}
|
|
`;
|
|
|
|
detailEl.classList.remove("hidden");
|
|
|
|
const closeBtn = document.getElementById("timer-details-close");
|
|
if (closeBtn) {
|
|
closeBtn.addEventListener("click", () => {
|
|
detailOpenFor = null;
|
|
detailEl.classList.add("hidden");
|
|
});
|
|
}
|
|
|
|
updateDetailsPanel();
|
|
}
|
|
|
|
setInterval(() => {
|
|
updateMainTimer();
|
|
updateSmallTimers();
|
|
updateDetailsPanel();
|
|
}, 1000);
|
|
|
|
updateMainTimer();
|
|
updateSmallTimers();
|
|
|
|
document.querySelectorAll(".small-circle").forEach(circle => {
|
|
circle.addEventListener("click", () => {
|
|
const timerId = circle.getAttribute("data-timer");
|
|
const timer = timersData.find(t => t.id === timerId);
|
|
if (timer) renderDetails(timer);
|
|
});
|
|
});
|
|
});
|
|
|
|
document.getElementById("fullscreen-btn").addEventListener("click", () => {
|
|
if (!document.fullscreenElement) {
|
|
document.documentElement.requestFullscreen();
|
|
} else {
|
|
document.exitFullscreen();
|
|
}
|
|
});
|
|
|
|
const cantinaAudio = document.getElementById("cantina-audio");
|
|
const circleContainer = document.querySelector(".circle-container");
|
|
|
|
function playCantina() {
|
|
if (!cantinaAudio) return;
|
|
cantinaAudio.currentTime = 0;
|
|
cantinaAudio.play().catch(err => console.warn("Cantina playback blocked:", err));
|
|
}
|
|
|
|
if (circleContainer) {
|
|
circleContainer.addEventListener("click", playCantina);
|
|
}
|
|
|
|
function scheduleNextFullHour() {
|
|
const now = new Date();
|
|
const next = new Date(now);
|
|
next.setMinutes(0, 0, 0);
|
|
if (next <= now) {
|
|
next.setHours(next.getHours() + 1);
|
|
}
|
|
const delay = next - now;
|
|
setTimeout(() => {
|
|
playCantina();
|
|
scheduleNextFullHour();
|
|
}, delay);
|
|
}
|
|
|
|
scheduleNextFullHour();
|
|
|
|
const overlay = document.getElementById("twitter-overlay");
|
|
|
|
function showOverlay() {
|
|
overlay.style.display = "flex";
|
|
const gongVideo = document.getElementById("gong-video-fullscreen");
|
|
if (gongVideo) {
|
|
gongVideo.currentTime = 0;
|
|
gongVideo.play().catch(err => console.warn("Autoplay-Fehler:", err));
|
|
}
|
|
setTimeout(() => {
|
|
overlay.style.display = "none";
|
|
}, 60000);
|
|
}
|
|
|
|
(() => {
|
|
const now = new Date();
|
|
const target = new Date();
|
|
target.setHours(23, 40, 0, 0);
|
|
|
|
if (now > target && now - target < 60000) {
|
|
showOverlay();
|
|
} else if (now <= target) {
|
|
const delay = target - now;
|
|
setTimeout(showOverlay, delay);
|
|
}
|
|
})();
|