website/assets/index.js
2024-11-05 08:18:36 +01:00

346 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// animation speeds cause why not
const TYPING_SPEED = 50;
const ERASE_SPEED = 70;
const STATUS_UPDATE_INTERVAL = 60000; // 1min updates
const TIME_UPDATE_INTERVAL = 1; // 1ms updates (yeah its bad)
// status warning stuff
const LATENCY_WARNING_THRESHOLD = 500000;
// lazy group name mapping
const GROUP_NAMES = {
4: "INFRASTRUCTURE",
5: "SERVICES",
6: "API",
};
// does the fancy typing animation for nav labels
// its way overcomplicated but whatever
function writeoutnavlabel(label) {
if (label.getAttribute("data-animating") === "true") return;
const text = label.getAttribute("data-text") || label.textContent;
label.setAttribute("data-text", text);
label.textContent = "";
label.style.opacity = "1";
label.setAttribute("data-animating", "true");
let i = 0;
let animationInterval = setInterval(() => {
if (i < text.length) {
label.textContent += text.charAt(i);
i++;
} else {
clearInterval(animationInterval);
label.setAttribute("data-animating", "false");
const icon = label.closest(".nav-icon");
if (icon) {
icon.isAnimating = false;
icon.hoverAnimationDone = true;
}
}
}, TYPING_SPEED);
label.setAttribute("data-animation-interval", animationInterval);
}
// erases the nav labels
// just as terrible as the typing but matches
function eraselabel(label) {
if (label.getAttribute("data-animating") === "true") {
const existingInterval = label.getAttribute("data-animation-interval");
if (existingInterval) clearInterval(existingInterval);
}
label.setAttribute("data-animating", "true");
let eraseInterval = setInterval(() => {
const currentText = label.textContent;
if (currentText.length > 0) {
label.textContent = currentText.slice(0, -1);
} else {
clearInterval(eraseInterval);
label.setAttribute("data-animating", "false");
label.style.opacity = "0";
}
}, ERASE_SPEED);
label.setAttribute("data-animation-interval", eraseInterval);
}
// we get the client info from the headers
// unclean but it works
function updateClientInfo() {
const locationElement = document.querySelector(".location");
if (!locationElement) {
console.warn("Location element not found in DOM");
return;
}
locationElement.classList.add("loading");
fetch(window.location.href)
.then((response) => {
const clientInfo = response.headers.get("X-Client-Info");
if (!clientInfo) {
throw new Error("X-Client-Info header not present");
}
const cleanClientInfo = clientInfo
.replace(/\u00C2\u00B5s/g, "µs")
.replace(/µs/g, "µs")
.trim();
requestAnimationFrame(() => {
locationElement.textContent = cleanClientInfo;
locationElement.classList.remove("loading");
locationElement.classList.add("loaded");
});
})
.catch((error) => {
console.error("Failed to update client info:", error);
requestAnimationFrame(() => {
locationElement.textContent = "CLIENT INFO UNAVAILABLE";
locationElement.classList.remove("loading");
locationElement.classList.add("loaded", "error");
});
});
}
// updates the time display
// probably updates too much but eh
function updateTime() {
const now = new Date();
const timeString = now
.toLocaleString("en-US", {
month: "2-digit",
day: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
fractionalSecondDigits: 3,
hour12: true,
})
.toUpperCase();
document.getElementById("current-time").textContent = timeString;
}
// bunch of status helper functions for formatting and stuff
function calculateAverageUptime(services) {
if (!services || services.length === 0) return 0;
const totalUptime = services.reduce(
(sum, service) => sum + service.online_24_hours,
0,
);
return (totalUptime / services.length).toFixed(2);
}
function formatLatency(latency) {
return (Math.round((latency / 1000) * 10) / 10).toFixed(1);
}
function formatUptime(uptime) {
return uptime.toFixed(2);
}
function getStatusClass(service) {
if (!service.online) return "status-offline";
if (service.latency > LATENCY_WARNING_THRESHOLD) return "status-warning";
return "status-online";
}
function getGroupName(groupId) {
return GROUP_NAMES[groupId] || `GROUP ${groupId}`;
}
// builds all the html for status page
// groups everything by group_id cause organization
function createStatusHTML(services) {
const groups = services.reduce((acc, service) => {
const groupId = service.group_id;
if (!acc[groupId]) acc[groupId] = [];
acc[groupId].push(service);
return acc;
}, {});
return Object.entries(groups)
.map(
([groupId, groupServices]) => `
<div class="status-group">
<div class="subsection-title">${getGroupName(parseInt(groupId))}</div>
${groupServices
.map(
(service) => `
<div class="status-item">
<div class="entry" data-service="${service.permalink}">
<span class="status-indicator ${getStatusClass(service)}"></span>
<span class="service-name">${service.name.toUpperCase()}</span>
<span class="service-status">${service.online ? "OPERATIONAL" : "OFFLINE"}</span>
</div>
<div class="status-details">
<div class="status-metric">Latency: ${formatLatency(service.latency)}ms</div>
<div class="status-metric">Uptime 24h: ${formatUptime(service.online_24_hours)}%</div>
<div class="status-metric">7-day Uptime: ${formatUptime(service.online_7_days)}%</div>
</div>
</div>
`,
)
.join("")}
</div>
`,
)
.join("");
}
// main status updater
// fetches and displays all service statuses
function updateStatus() {
const statusContainer = document.getElementById("status-container");
const averageUptimeElement = document.getElementById("average-uptime");
fetch("https://status.elia.network/api/services")
.then((response) => response.json())
.then((data) => {
statusContainer.innerHTML = createStatusHTML(data);
const averageUptime = calculateAverageUptime(data);
averageUptimeElement.textContent = `UPTIME ${averageUptime}%`;
document.querySelectorAll(".status-item").forEach((item) => {
item.addEventListener("click", function () {
const details = this.querySelector(".status-details");
details.classList.toggle("expanded");
});
});
})
.catch((error) => {
statusContainer.innerHTML = `
<div class="error-message">
UNABLE TO FETCH STATUS DATA
<div style="font-size: 0.8em; margin-top: 5px;">
${error.message}
</div>
</div>
`;
averageUptimeElement.textContent = "UPTIME ERROR";
});
}
// handles expanding/collapsing project items
// closes others when opening new one
document.querySelectorAll(".project-item").forEach((item) => {
item.addEventListener("click", function () {
const content = this.querySelector(".project-content");
const icon = this.querySelector(".expand-icon");
document.querySelectorAll(".project-item").forEach((otherItem) => {
if (otherItem !== item) {
otherItem
.querySelector(".project-content")
.classList.remove("expanded");
otherItem.querySelector(".expand-icon").textContent = "+";
}
});
content.classList.toggle("expanded");
icon.textContent = content.classList.contains("expanded") ? "" : "+";
});
});
// smooth scrolls to sections when clicking nav
// better than jumping around
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener("click", function (e) {
e.preventDefault();
document.querySelector(this.getAttribute("href")).scrollIntoView({
behavior: "smooth",
});
});
});
// handles active nav icon updates on scroll
// shows which section youre looking at
window.addEventListener("scroll", () => {
let current = "";
document.querySelectorAll("section").forEach((section) => {
const sectionTop = section.offsetTop;
if (scrollY >= sectionTop - 60) {
current = section.getAttribute("id");
}
});
document.querySelectorAll(".nav-icon").forEach((icon) => {
const label = icon.querySelector(".nav-label");
const isCurrentSection = icon.getAttribute("href") === `#${current}`;
const wasActive = icon.classList.contains("active");
if (isCurrentSection && !wasActive) {
icon.classList.add("active");
label.style.opacity = "1";
if (!icon.isAnimating && !icon.hoverAnimationDone) {
writeoutnavlabel(label);
icon.hoverAnimationDone = true;
}
} else if (!isCurrentSection && wasActive) {
icon.classList.remove("active");
if (!icon.isAnimating) {
eraselabel(label);
icon.isAnimating = false;
icon.hoverAnimationDone = false;
}
}
});
});
// nav icon hover stuff
// does the typing animation when hovering
document.querySelectorAll(".nav-icon").forEach((icon) => {
icon.hoverAnimationDone = false;
icon.isAnimating = false;
icon.addEventListener("mouseenter", () => {
if (!icon.classList.contains("active") && !icon.isAnimating) {
const label = icon.querySelector(".nav-label");
writeoutnavlabel(label);
icon.isAnimating = true;
}
});
icon.addEventListener("mouseleave", () => {
if (!icon.classList.contains("active")) {
const label = icon.querySelector(".nav-label");
eraselabel(label);
icon.isAnimating = false;
icon.hoverAnimationDone = false;
}
});
icon.addEventListener("click", () => {
const label = icon.querySelector(".nav-label");
label.style.opacity = "1";
});
});
// sets everything up when page loads
// starts all the intervals and stuff
document.addEventListener("DOMContentLoaded", () => {
updateTime();
setInterval(updateTime, TIME_UPDATE_INTERVAL);
updateStatus();
setInterval(updateStatus, STATUS_UPDATE_INTERVAL);
updateClientInfo();
const activeNavIcon = document.querySelector(".nav-icon.active");
if (activeNavIcon) {
const label = activeNavIcon.querySelector(".nav-label");
if (label) writeoutnavlabel(label);
}
// fancy console log for style points
console.log(
"%cELIA.NETWORK",
"color: #2a5a5a; font-size: 24px; font-weight: bold; text-shadow: 2px 2px 4px rgba(42, 90, 90, 0.3);",
);
});