Слияние ui -> main #1
@@ -53,6 +53,9 @@ SITE_DIR = Path(__file__).parent / "site"
|
||||
# SkinRestorer skins (shared with TG bot)
|
||||
SKINS_DIR = Path("/root/python/site/skins")
|
||||
|
||||
# Minecraft server status (same host the TG bot pings)
|
||||
MC_STATUS_HOST = os.environ.get("MC_STATUS_HOST", "mc.zernmc.ru")
|
||||
|
||||
# Mirror configuration
|
||||
LAUNCHER_MIRRORS = {
|
||||
"main": "https://api.zern.cc", # primary
|
||||
@@ -966,6 +969,11 @@ for sub in ("css", "js", "img"):
|
||||
mount_dir.mkdir(parents=True, exist_ok=True)
|
||||
app.mount(f"/{sub}", StaticFiles(directory=mount_dir), name=f"site-{sub}")
|
||||
|
||||
# Server highlights media (screenshots / clips for the landing carousel)
|
||||
HIGHLIGHTS_DIR = SITE_DIR / "highlights"
|
||||
HIGHLIGHTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
app.mount("/highlights", StaticFiles(directory=HIGHLIGHTS_DIR), name="site-highlights")
|
||||
|
||||
|
||||
# Monkey patch to catch invalid HTTP requests
|
||||
original_data_received = HttpToolsProtocol.data_received
|
||||
@@ -1054,6 +1062,44 @@ async def get_skin(filename: str, request: Request):
|
||||
raise HTTPException(status_code=500, detail="Ошибка чтения скина")
|
||||
|
||||
|
||||
@app.get("/api/status")
|
||||
async def api_server_status():
|
||||
"""Live Minecraft server status via mcstatus ping (same host as TG bot)"""
|
||||
try:
|
||||
from mcstatus import JavaServer
|
||||
server = JavaServer.lookup(f"{MC_STATUS_HOST}")
|
||||
status = await server.async_status()
|
||||
|
||||
players_list = []
|
||||
try:
|
||||
query = await server.async_query()
|
||||
if query.players.names:
|
||||
players_list = query.players.names
|
||||
elif query.players.online > 0 and query.players.sample:
|
||||
players_list = [p.name for p in query.players.sample]
|
||||
except Exception:
|
||||
if status.players.sample:
|
||||
players_list = [p.name for p in status.players.sample]
|
||||
|
||||
version = getattr(status.version, "name", None) or "unknown"
|
||||
motd = status.description
|
||||
|
||||
return {
|
||||
"online": True,
|
||||
"players_online": status.players.online,
|
||||
"players_max": status.players.max,
|
||||
"players_list": ", ".join(players_list) if players_list else "никого нет",
|
||||
"players": players_list,
|
||||
"motd": motd,
|
||||
"version": version if isinstance(version, str) else str(version),
|
||||
"updated": datetime.now().strftime("%H:%M:%S")
|
||||
}
|
||||
except asyncio.TimeoutError:
|
||||
return {"online": False, "error": "Таймаут подключения к серверу", "updated": datetime.now().strftime("%H:%M:%S")}
|
||||
except Exception as e:
|
||||
return {"online": False, "error": f"{type(e).__name__}: {e}", "updated": datetime.now().strftime("%H:%M:%S")}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
"""Health check endpoint"""
|
||||
|
||||
@@ -159,7 +159,7 @@ class LoggingMiddleware(BaseHTTPMiddleware):
|
||||
# Skip rate limiting for pack file downloads (direct high-speed serving)
|
||||
if path.startswith("/pack/") and "/file/" in path:
|
||||
is_file_download = True
|
||||
elif path.startswith(("/skin/", "/css/", "/js/", "/img/")):
|
||||
elif path.startswith(("/skin/", "/css/", "/js/", "/img/", "/highlights/")):
|
||||
# SkinRestorer skins + landing site assets must not be rate-limited
|
||||
is_file_download = True
|
||||
else:
|
||||
|
||||
@@ -346,6 +346,209 @@ ul { list-style: none; }
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* ---------- Server status ---------- */
|
||||
|
||||
.server {
|
||||
background:
|
||||
radial-gradient(800px 400px at 15% 10%, rgba(233, 69, 96, 0.08), transparent 60%),
|
||||
radial-gradient(800px 400px at 85% 90%, rgba(233, 69, 96, 0.05), transparent 60%);
|
||||
}
|
||||
|
||||
.server-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.server-card {
|
||||
background: rgba(22, 22, 31, 0.85);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 30px 28px;
|
||||
transition: transform .25s ease, border-color .25s ease;
|
||||
}
|
||||
|
||||
.server-card:hover { transform: translateY(-4px); border-color: rgba(233, 69, 96, 0.35); }
|
||||
|
||||
.server-card-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.server-card-value {
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.server-card-sub { font-size: 13px; color: var(--text-secondary); margin-top: 10px; }
|
||||
|
||||
.server-season { color: var(--text-secondary); font-size: 18px; font-weight: 600; }
|
||||
|
||||
.status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
flex: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-dot--online { background: #22c55e; box-shadow: 0 0 16px rgba(34, 197, 94, 0.8); animation: pulse 2s infinite; }
|
||||
.status-dot--offline { background: #ef4444; box-shadow: 0 0 16px rgba(239, 68, 68, 0.8); }
|
||||
.status-dot--loading { background: var(--text-secondary); animation: pulse 1.2s infinite; }
|
||||
|
||||
.status-text--online { color: #22c55e; }
|
||||
.status-text--offline { color: #ef4444; }
|
||||
|
||||
/* ---------- Highlights carousel ---------- */
|
||||
|
||||
.carousel {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.carousel-track {
|
||||
display: flex;
|
||||
transition: transform .6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.carousel-slide {
|
||||
flex: 0 0 100%;
|
||||
min-width: 100%;
|
||||
position: relative;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.carousel-slide img,
|
||||
.carousel-slide video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 640px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.carousel-caption {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
bottom: 20px;
|
||||
background: rgba(7, 7, 10, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.carousel-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 5;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(7, 7, 10, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
color: var(--text);
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background .2s ease, transform .15s ease, color .2s ease;
|
||||
}
|
||||
|
||||
.carousel-btn:hover { background: var(--accent); color: #fff; }
|
||||
|
||||
.carousel-btn--prev { left: 16px; }
|
||||
.carousel-btn--next { right: 16px; }
|
||||
|
||||
.carousel-btn:active { transform: translateY(-50%) scale(0.92); }
|
||||
|
||||
.carousel-dots {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 18px 0;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.carousel-dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
cursor: pointer;
|
||||
transition: background .2s ease, transform .2s ease;
|
||||
}
|
||||
|
||||
.carousel-dot.active { background: var(--accent); transform: scale(1.3); }
|
||||
|
||||
/* ---------- Season theme grid ---------- */
|
||||
|
||||
.theme {
|
||||
background: var(--bg-surface);
|
||||
border-top: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.theme-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
transition: transform .25s ease, border-color .25s ease, background .25s ease;
|
||||
}
|
||||
|
||||
.theme-card:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: rgba(233, 69, 96, 0.35);
|
||||
background: #1a1a25;
|
||||
}
|
||||
|
||||
.theme-tag {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
background: rgba(233, 69, 96, 0.12);
|
||||
padding: 5px 10px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.theme-card h3 { font-size: 20px; font-weight: 700; margin-bottom: 10px; }
|
||||
.theme-card p { font-size: 14px; color: var(--text-secondary); font-weight: 300; }
|
||||
|
||||
/* ---------- Features ---------- */
|
||||
|
||||
.features-grid {
|
||||
@@ -595,16 +798,19 @@ ul { list-style: none; }
|
||||
/* ---------- Responsive ---------- */
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.features-grid, .news-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.features-grid, .news-grid, .theme-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.server-stats { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.nav-links { display: none; }
|
||||
.section { padding: 90px 20px; }
|
||||
.features-grid, .news-grid { grid-template-columns: 1fr; }
|
||||
.features-grid, .news-grid, .theme-grid { grid-template-columns: 1fr; }
|
||||
.server-stats { grid-template-columns: 1fr; }
|
||||
.hero { padding-top: 140px; }
|
||||
.hero-diamond--1 { right: -10%; }
|
||||
.download-card { padding: 26px; }
|
||||
.carousel-btn { width: 42px; height: 42px; font-size: 22px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 107 KiB |
@@ -20,6 +20,9 @@
|
||||
<span>Zern</span>
|
||||
</a>
|
||||
<ul class="nav-links">
|
||||
<li><a href="#server">Server</a></li>
|
||||
<li><a href="#highlights">Highlights</a></li>
|
||||
<li><a href="#theme">Season</a></li>
|
||||
<li><a href="#features">Features</a></li>
|
||||
<li><a href="#download">Download</a></li>
|
||||
<li><a href="#news">News</a></li>
|
||||
@@ -58,9 +61,50 @@
|
||||
|
||||
<main>
|
||||
|
||||
<section class="section server" id="server">
|
||||
<div class="section-head">
|
||||
<span class="section-index">01 / 06</span>
|
||||
<h2 class="section-title">The server is <span class="accent">alive.</span></h2>
|
||||
<p class="section-sub">ZernMC is a custom Minecraft server with its own lore, hand-written plugins and a living community. This is what's happening right now.</p>
|
||||
</div>
|
||||
|
||||
<div class="server-stats">
|
||||
<div class="server-card">
|
||||
<div class="server-card-label">Status</div>
|
||||
<div class="server-card-value" id="srv-status"><span class="status-dot status-dot--loading"></span> Checking…</div>
|
||||
<div class="server-card-sub" id="srv-version">1.20.1 / OBТ</div>
|
||||
</div>
|
||||
<div class="server-card">
|
||||
<div class="server-card-label">Players online</div>
|
||||
<div class="server-card-value" id="srv-players">— / —</div>
|
||||
<div class="server-card-sub">mc.zernmc.ru</div>
|
||||
</div>
|
||||
<div class="server-card">
|
||||
<div class="server-card-label">Mode</div>
|
||||
<div class="server-card-value">Survival <span class="server-season">· Season Prologue</span></div>
|
||||
<div class="server-card-sub">1.21.1 NeoForge · Create: Aeronautics</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section highlights" id="highlights">
|
||||
<div class="section-head">
|
||||
<span class="section-index">02 / 06</span>
|
||||
<h2 class="section-title">Server <span class="accent">highlights.</span></h2>
|
||||
<p class="section-sub">Moments captured on the ZernMC server — wars, builds and chaos.</p>
|
||||
</div>
|
||||
|
||||
<div class="carousel">
|
||||
<div class="carousel-track" id="carousel"></div>
|
||||
<button class="carousel-btn carousel-btn--prev" id="carouselPrev" aria-label="Previous">‹</button>
|
||||
<button class="carousel-btn carousel-btn--next" id="carouselNext" aria-label="Next">›</button>
|
||||
<div class="carousel-dots" id="carouselDots"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section features" id="features">
|
||||
<div class="section-head">
|
||||
<span class="section-index">01 / 05</span>
|
||||
<span class="section-index">03 / 06</span>
|
||||
<h2 class="section-title">Everything <span class="accent">you need.</span></h2>
|
||||
<p class="section-sub">A launcher that does the heavy lifting, so you can just play.</p>
|
||||
</div>
|
||||
@@ -113,10 +157,51 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section theme" id="theme">
|
||||
<div class="section-head section-head--center">
|
||||
<span class="section-index">04 / 06</span>
|
||||
<h2 class="section-title">What's inside <span class="accent">the season.</span></h2>
|
||||
<p class="section-sub">Prologue — a modded season built around Create: Aeronautics, war and survival. OBT is the testing ground for the whole idea.</p>
|
||||
</div>
|
||||
|
||||
<div class="theme-grid">
|
||||
<article class="theme-card">
|
||||
<span class="theme-tag">OBT · 1.20.1 Forge</span>
|
||||
<h3>War in Zarya</h3>
|
||||
<p>Two-day event set in the city of Zarya: the Fenix group attacks, Lastochka defends, and civilians pick a side — or rise up. Military rules apply.</p>
|
||||
</article>
|
||||
<article class="theme-card">
|
||||
<span class="theme-tag">OBT · test stage</span>
|
||||
<h3>Open beta server</h3>
|
||||
<p>The beta is an event server: city battles, an atomic-reactor city-life scenario and rule sets that will shape the real season.</p>
|
||||
</article>
|
||||
<article class="theme-card">
|
||||
<span class="theme-tag">Season · 1.21.1 NeoForge</span>
|
||||
<h3>Ships in the sky</h3>
|
||||
<p>Create: Aeronautics gives you airships, submarines and flying contraptions. Trains, diesel engines, big cannons and a nuclear reactor to master.</p>
|
||||
</article>
|
||||
<article class="theme-card">
|
||||
<span class="theme-tag">Season · Prologue</span>
|
||||
<h3>A year-long run</h3>
|
||||
<p>Each season is a marathon, not a sprint. Start this chapter from day one and shape it with the community.</p>
|
||||
</article>
|
||||
<article class="theme-card">
|
||||
<span class="theme-tag">Server</span>
|
||||
<h3>Own lore & plugins</h3>
|
||||
<p>Hand-written plugins, events written for the server story, and systems the launcher integrates with out of the box.</p>
|
||||
</article>
|
||||
<article class="theme-card">
|
||||
<span class="theme-tag">Community</span>
|
||||
<h3>Voice & friends</h3>
|
||||
<p>Simple Voice Chat in-game, a friends system in the launcher and a Telegram community that runs the show — decisions are voted on by players.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section download" id="download">
|
||||
<div class="download-inner">
|
||||
<div class="section-head section-head--center">
|
||||
<span class="section-index">02 / 05</span>
|
||||
<span class="section-index">05 / 06</span>
|
||||
<h2 class="section-title">Get <span class="accent">Zern.</span></h2>
|
||||
<p class="section-sub">Download the launcher for Windows and join the ZernMC server.</p>
|
||||
</div>
|
||||
@@ -144,7 +229,7 @@
|
||||
|
||||
<section class="section news" id="news">
|
||||
<div class="section-head">
|
||||
<span class="section-index">03 / 05</span>
|
||||
<span class="section-index">06 / 06</span>
|
||||
<h2 class="section-title">What's <span class="accent">new.</span></h2>
|
||||
<p class="section-sub">Latest changelogs and updates from the Zern team.</p>
|
||||
</div>
|
||||
|
||||
@@ -63,6 +63,124 @@
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -150,7 +268,7 @@
|
||||
}
|
||||
|
||||
function initReveal() {
|
||||
const els = document.querySelectorAll('.section-head, .feature-card, .download-card, .news-card');
|
||||
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;
|
||||
@@ -175,6 +293,9 @@
|
||||
loadLauncherInfo();
|
||||
loadMirrors();
|
||||
loadNews();
|
||||
loadServerStatus();
|
||||
setInterval(loadServerStatus, 15000);
|
||||
initCarousel();
|
||||
initNav();
|
||||
initParallax();
|
||||
initReveal();
|
||||
|
||||