Please enter the commit message for your changes. Lines starting
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
This commit is contained in:
188
volumes/web/static/js/mandelbrot.js
Normal file
188
volumes/web/static/js/mandelbrot.js
Normal file
@@ -0,0 +1,188 @@
|
||||
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);
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user