c628397850
- server status cards (online/players/version) via /api/status mcstatus ping - highlights carousel ported from old site (/highlights/ static media) - OBT (war in Zarya, open beta event server) + Season (Prologue, Create: Aeronautics, year-long) sections - nav updated, sections renumbered 01-06
305 lines
10 KiB
JavaScript
305 lines
10 KiB
JavaScript
(() => {
|
|
'use strict';
|
|
|
|
const API = ''; // same origin (root -> launcher server)
|
|
|
|
function fmtSize(bytes) {
|
|
if (!bytes) return '—';
|
|
const units = ['B', 'KB', 'MB', 'GB'];
|
|
let i = 0;
|
|
let n = bytes;
|
|
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
|
|
return n.toFixed(1) + ' ' + units[i];
|
|
}
|
|
|
|
async function loadLauncherInfo() {
|
|
try {
|
|
const res = await fetch(API + '/launcher/info');
|
|
if (!res.ok) return;
|
|
const data = await res.json();
|
|
const version = data.current_version || '—';
|
|
|
|
const heroVersion = document.getElementById('hero-version');
|
|
const heroSize = document.getElementById('hero-size');
|
|
const dlVersion = document.getElementById('download-version');
|
|
const dlSize = document.getElementById('download-size');
|
|
const dlBtn = document.getElementById('download-btn');
|
|
|
|
if (heroVersion) heroVersion.textContent = version;
|
|
if (dlVersion) dlVersion.textContent = version;
|
|
|
|
if (data.files && data.files.zips && data.files.zips.length) {
|
|
const latest = data.files.zips[0];
|
|
if (heroSize) heroSize.textContent = fmtSize(latest.size);
|
|
if (dlSize) dlSize.textContent = fmtSize(latest.size);
|
|
}
|
|
|
|
if (dlBtn && data.new_format && data.new_format.download_url) {
|
|
dlBtn.href = API + data.new_format.download_url;
|
|
}
|
|
} catch (e) {
|
|
console.warn('[site] failed to load launcher info', e);
|
|
}
|
|
}
|
|
|
|
async function loadMirrors() {
|
|
try {
|
|
const res = await fetch(API + '/launcher/mirrors');
|
|
if (!res.ok) return;
|
|
const data = await res.json();
|
|
const container = document.getElementById('mirrors');
|
|
if (!container || !data.mirrors || !data.mirrors.length) return;
|
|
|
|
const tags = data.mirrors.slice(0, 5).map((m) => {
|
|
const a = document.createElement('a');
|
|
a.className = 'mirror-tag';
|
|
a.href = (m.url || '') + '/launcher/download/latest';
|
|
a.textContent = m.name || m;
|
|
return a;
|
|
});
|
|
tags.forEach((t) => container.appendChild(t));
|
|
} catch (e) {
|
|
console.warn('[site] failed to load mirrors', e);
|
|
}
|
|
}
|
|
|
|
async function loadServerStatus() {
|
|
const statusEl = document.getElementById('srv-status');
|
|
const playersEl = document.getElementById('srv-players');
|
|
const versionEl = document.getElementById('srv-version');
|
|
const dot = document.querySelector('#srv-status .status-dot');
|
|
if (!statusEl) return;
|
|
|
|
try {
|
|
const res = await fetch(API + '/api/status');
|
|
const data = await res.json();
|
|
|
|
if (data.online) {
|
|
if (dot) dot.className = 'status-dot status-dot--online';
|
|
statusEl.innerHTML = '<span class="status-dot status-dot--online"></span><span class="status-text--online">Online</span>';
|
|
if (playersEl) playersEl.textContent = `${data.players_online} / ${data.players_max}`;
|
|
if (versionEl && data.version) versionEl.textContent = data.version;
|
|
} else {
|
|
if (dot) dot.className = 'status-dot status-dot--offline';
|
|
statusEl.innerHTML = '<span class="status-dot status-dot--offline"></span><span class="status-text--offline">Offline</span>';
|
|
if (playersEl) playersEl.textContent = '— / —';
|
|
}
|
|
} catch (e) {
|
|
if (dot) dot.className = 'status-dot status-dot--offline';
|
|
statusEl.innerHTML = '<span class="status-dot status-dot--offline"></span><span>Unavailable</span>';
|
|
}
|
|
}
|
|
|
|
const HIGHLIGHTS = [
|
|
{ file: '1.jpg', type: 'image', caption: 'Zern' },
|
|
{ file: '2.mp4', type: 'video', caption: null },
|
|
{ file: '3.jpg', type: 'image', caption: 'Это честно не я' },
|
|
{ file: '4.mp4', type: 'video', caption: null },
|
|
{ file: '5.jpg', type: 'image', caption: 'Спавн ТЦ (старое)' },
|
|
{ file: '6.jpg', type: 'image', caption: 'Спавн склад (старое)' },
|
|
{ file: '7.jpg', type: 'image', caption: null },
|
|
{ file: '8.jpg', type: 'image', caption: null },
|
|
{ file: '9.jpg', type: 'image', caption: null },
|
|
{ file: '10.jpg', type: 'image', caption: null },
|
|
{ file: '11.jpg', type: 'image', caption: null },
|
|
{ file: '12.jpg', type: 'image', caption: null },
|
|
{ file: '13.jpg', type: 'image', caption: null },
|
|
{ file: '14.jpg', type: 'image', caption: null },
|
|
{ file: '15.jpg', type: 'image', caption: null },
|
|
];
|
|
|
|
function initCarousel() {
|
|
const track = document.getElementById('carousel');
|
|
const dotsWrap = document.getElementById('carouselDots');
|
|
const prev = document.getElementById('carouselPrev');
|
|
const next = document.getElementById('carouselNext');
|
|
if (!track) return;
|
|
|
|
HIGHLIGHTS.forEach((h, i) => {
|
|
const slide = document.createElement('div');
|
|
slide.className = 'carousel-slide';
|
|
|
|
if (h.type === 'video') {
|
|
const vid = document.createElement('video');
|
|
vid.src = '/highlights/' + h.file;
|
|
vid.controls = true;
|
|
vid.autoplay = true;
|
|
vid.loop = true;
|
|
vid.muted = true;
|
|
vid.playsInline = true;
|
|
slide.appendChild(vid);
|
|
} else {
|
|
const img = document.createElement('img');
|
|
img.src = '/highlights/' + h.file;
|
|
img.alt = h.caption || 'Zern highlight';
|
|
img.loading = 'lazy';
|
|
slide.appendChild(img);
|
|
}
|
|
|
|
if (h.caption) {
|
|
const cap = document.createElement('span');
|
|
cap.className = 'carousel-caption';
|
|
cap.textContent = h.caption;
|
|
slide.appendChild(cap);
|
|
}
|
|
|
|
track.appendChild(slide);
|
|
|
|
const dot = document.createElement('span');
|
|
dot.className = 'carousel-dot' + (i === 0 ? ' active' : '');
|
|
dot.addEventListener('click', () => goto(i));
|
|
dotsWrap.appendChild(dot);
|
|
});
|
|
|
|
let index = 0;
|
|
let timer = null;
|
|
|
|
function goto(i) {
|
|
index = (i + HIGHLIGHTS.length) % HIGHLIGHTS.length;
|
|
track.style.transform = 'translateX(-' + index * 100 + '%)';
|
|
dotsWrap.querySelectorAll('.carousel-dot').forEach((d, k) => {
|
|
d.classList.toggle('active', k === index);
|
|
});
|
|
}
|
|
|
|
function nextSlide() { goto(index + 1); }
|
|
function prevSlide() { goto(index - 1); }
|
|
|
|
function start() {
|
|
stop();
|
|
timer = setInterval(nextSlide, 6500);
|
|
}
|
|
function stop() {
|
|
if (timer) { clearInterval(timer); timer = null; }
|
|
}
|
|
|
|
if (prev) prev.addEventListener('click', () => { prevSlide(); start(); });
|
|
if (next) next.addEventListener('click', () => { nextSlide(); start(); });
|
|
track.addEventListener('mouseenter', stop);
|
|
track.addEventListener('mouseleave', start);
|
|
|
|
start();
|
|
}
|
|
|
|
function stripMarkup(text) {
|
|
if (!text) return '';
|
|
return text
|
|
.replace(/\[[^\]]*photo=[^\]]*\]/g, '')
|
|
.replace(/\[[^\]]*\]/g, '')
|
|
.replace(/\*\*\*/g, '')
|
|
.replace(/\*\*/g, '')
|
|
.replace(/[*_~`#]/g, '')
|
|
.replace(/\n+/g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
async function loadNews() {
|
|
const grid = document.getElementById('news-grid');
|
|
if (!grid) return;
|
|
|
|
try {
|
|
const res = await fetch(API + '/news');
|
|
if (!res.ok) throw new Error('news fetch failed');
|
|
const data = await res.json();
|
|
const news = (data.news || []).slice(0, 6);
|
|
if (!news.length) { grid.innerHTML = '<p class="section-sub">No news yet.</p>'; return; }
|
|
|
|
grid.innerHTML = '';
|
|
news.forEach((n) => {
|
|
const card = document.createElement('article');
|
|
card.className = 'news-card';
|
|
|
|
const tags = document.createElement('div');
|
|
tags.className = 'news-tags';
|
|
if (n.type) {
|
|
const t = document.createElement('span');
|
|
t.className = 'news-tag';
|
|
t.textContent = n.type;
|
|
tags.appendChild(t);
|
|
}
|
|
if (n.version) {
|
|
const v = document.createElement('span');
|
|
v.className = 'news-tag news-tag--version';
|
|
v.textContent = n.version;
|
|
tags.appendChild(v);
|
|
}
|
|
|
|
const title = document.createElement('h3');
|
|
title.textContent = n.title || 'Update';
|
|
|
|
const body = document.createElement('p');
|
|
const text = stripMarkup(n.body);
|
|
body.textContent = text.length > 220 ? text.slice(0, 220) + '…' : text;
|
|
|
|
card.appendChild(tags);
|
|
card.appendChild(title);
|
|
card.appendChild(body);
|
|
grid.appendChild(card);
|
|
});
|
|
} catch (e) {
|
|
console.warn('[site] failed to load news', e);
|
|
grid.innerHTML = '<p class="section-sub">News is temporarily unavailable.</p>';
|
|
}
|
|
}
|
|
|
|
function initNav() {
|
|
const nav = document.getElementById('nav');
|
|
const onScroll = () => nav.classList.toggle('scrolled', window.scrollY > 24);
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
onScroll();
|
|
}
|
|
|
|
function initParallax() {
|
|
const els = document.querySelectorAll('[data-parallax]');
|
|
if (!els.length) return;
|
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
|
|
const onScroll = () => {
|
|
const y = window.scrollY;
|
|
if (y > window.innerHeight) return;
|
|
els.forEach((el) => {
|
|
const speed = parseFloat(el.dataset.parallax);
|
|
el.style.transform = 'translateY(' + (y * speed) + 'px)';
|
|
});
|
|
};
|
|
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
onScroll();
|
|
}
|
|
|
|
function initReveal() {
|
|
const els = document.querySelectorAll('.section-head, .feature-card, .download-card, .news-card, .server-card, .theme-card, .carousel');
|
|
if (!('IntersectionObserver' in window)) {
|
|
els.forEach((el) => el.classList.add('visible'));
|
|
return;
|
|
}
|
|
const io = new IntersectionObserver((entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('visible');
|
|
io.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, { threshold: 0.12 });
|
|
els.forEach((el) => { el.classList.add('reveal'); io.observe(el); });
|
|
}
|
|
|
|
function initYear() {
|
|
const el = document.getElementById('year');
|
|
if (el) el.textContent = new Date().getFullYear();
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadLauncherInfo();
|
|
loadMirrors();
|
|
loadNews();
|
|
loadServerStatus();
|
|
setInterval(loadServerStatus, 15000);
|
|
initCarousel();
|
|
initNav();
|
|
initParallax();
|
|
initReveal();
|
|
initYear();
|
|
});
|
|
})();
|