589 lines
18 KiB
HTML
589 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Startseite</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
background: #111;
|
|
color: #eee;
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
|
|
header {
|
|
background: #1a1a1a;
|
|
border-bottom: 2px solid #333;
|
|
padding: 16px 24px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
header h1 { margin: 0; font-size: 22px; }
|
|
|
|
#topNav {
|
|
background: #161616;
|
|
border-bottom: 1px solid #2a2a2a;
|
|
padding: 10px 24px;
|
|
display: flex;
|
|
gap: 18px;
|
|
flex-wrap: wrap;
|
|
}
|
|
#topNav a {
|
|
color: #6fbf73;
|
|
text-decoration: none;
|
|
font-size: 14px;
|
|
}
|
|
#topNav a:hover { text-decoration: underline; }
|
|
|
|
#loginArea { display: flex; gap: 8px; align-items: center; }
|
|
#loggedInArea { display: none; align-items: center; gap: 12px; }
|
|
|
|
input {
|
|
background: #222;
|
|
border: 1px solid #444;
|
|
color: #eee;
|
|
padding: 8px 10px;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
button {
|
|
background: #2c7a3d;
|
|
border: none;
|
|
color: white;
|
|
padding: 8px 14px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
}
|
|
button.secondary { background: #333; }
|
|
button.danger { background: #a83232; }
|
|
button:hover { opacity: 0.85; }
|
|
|
|
main {
|
|
max-width: 900px;
|
|
margin: 30px auto;
|
|
padding: 0 20px;
|
|
}
|
|
|
|
.play-banner {
|
|
background: linear-gradient(135deg, #1f3d2b, #142218);
|
|
border: 1px solid #2c7a3d;
|
|
border-radius: 10px;
|
|
padding: 24px;
|
|
margin-bottom: 30px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.play-banner h2 { margin: 0 0 6px 0; }
|
|
.play-banner p { margin: 0; color: #aaa; }
|
|
|
|
section.panel {
|
|
background: #1a1a1a;
|
|
border: 1px solid #333;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin-bottom: 24px;
|
|
}
|
|
section.panel h2 {
|
|
margin-top: 0;
|
|
border-bottom: 2px solid #333;
|
|
padding-bottom: 8px;
|
|
}
|
|
|
|
.news-columns { display: flex; gap: 24px; align-items: flex-start; }
|
|
.news-columns section.panel { flex: 1; min-width: 0; }
|
|
@media (max-width: 800px) {
|
|
.news-columns { flex-direction: column; }
|
|
}
|
|
|
|
.news-item {
|
|
border-bottom: 1px solid #2a2a2a;
|
|
padding: 14px 0;
|
|
}
|
|
.news-item:last-child { border-bottom: none; }
|
|
.news-item h3 { margin: 0 0 4px 0; }
|
|
.news-meta { color: #777; font-size: 12px; margin-bottom: 8px; }
|
|
.news-content { white-space: pre-wrap; line-height: 1.5; }
|
|
.badge {
|
|
display: inline-block;
|
|
background: #2c7a3d;
|
|
color: white;
|
|
border-radius: 4px;
|
|
padding: 2px 8px;
|
|
font-size: 12px;
|
|
font-weight: normal;
|
|
margin-left: 6px;
|
|
}
|
|
.news-item .danger { margin-top: 8px; font-size: 12px; padding: 4px 10px; }
|
|
|
|
#newsEmpty { color: #666; }
|
|
|
|
.admin-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 14px;
|
|
}
|
|
.admin-card {
|
|
background: #202020;
|
|
border: 1px solid #333;
|
|
border-radius: 8px;
|
|
padding: 18px;
|
|
text-decoration: none;
|
|
color: #eee;
|
|
display: block;
|
|
transition: border-color 0.15s;
|
|
}
|
|
.admin-card:hover { border-color: #2c7a3d; }
|
|
.admin-card .icon { font-size: 26px; margin-bottom: 8px; }
|
|
.admin-card h3 { margin: 0 0 4px 0; font-size: 16px; }
|
|
.admin-card p { margin: 0; color: #999; font-size: 13px; }
|
|
|
|
.form-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
|
|
textarea {
|
|
width: 100%;
|
|
min-height: 100px;
|
|
background: #222;
|
|
border: 1px solid #444;
|
|
color: #eee;
|
|
padding: 8px 10px;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
box-sizing: border-box;
|
|
font-family: inherit;
|
|
resize: vertical;
|
|
}
|
|
|
|
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 6px; }
|
|
.hidden { display: none !important; }
|
|
|
|
#registerOverlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0,0,0,0.7);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 20000;
|
|
}
|
|
#registerBox {
|
|
background: #1a1a1a;
|
|
border: 2px solid #444;
|
|
border-radius: 8px;
|
|
padding: 24px;
|
|
width: 320px;
|
|
}
|
|
#registerBox input {
|
|
width: 100%;
|
|
margin-bottom: 8px;
|
|
}
|
|
#registerBox .form-row {
|
|
margin-top: 10px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<h1>🎮 <span id="siteTitleText">Mein Multiplayer-Spiel</span> <span id="onlineCount" style="font-size:14px; color:#6fbf73; font-weight:normal; margin-left:12px;"></span></h1>
|
|
|
|
<div id="loginArea">
|
|
<input type="text" id="loginUser" placeholder="Benutzername">
|
|
<input type="password" id="loginPass" placeholder="Passwort">
|
|
<button onclick="doLogin()">Login</button>
|
|
<button class="secondary" onclick="showRegister()">Registrieren</button>
|
|
</div>
|
|
|
|
<div id="loggedInArea">
|
|
<span id="welcomeText"></span>
|
|
<a href="/profile.html" style="color:#6fbf73; text-decoration:none; font-size:14px;">👤 Profil</a>
|
|
<button class="secondary" onclick="doLogout()">Logout</button>
|
|
</div>
|
|
</header>
|
|
|
|
<nav id="topNav">
|
|
<a href="/uebersicht.html">📂 Übersicht</a>
|
|
<a href="/wuensche.html">Wünsche</a>
|
|
<a href="/umfragen.html">Umfragen</a>
|
|
<a href="/bestenliste.html">Bestenliste</a>
|
|
<a href="/auktionshaus.html">Auktionshaus</a>
|
|
<a href="/anmerkungen.html">Anmerkungen</a>
|
|
</nav>
|
|
|
|
<div id="registerOverlay" class="hidden">
|
|
<div id="registerBox">
|
|
<h2 style="margin-top:0;">Registrieren</h2>
|
|
<input type="text" id="regUser" placeholder="Benutzername">
|
|
<input type="password" id="regPass" placeholder="Passwort">
|
|
<input type="password" id="regPassConfirm" placeholder="Passwort wiederholen">
|
|
<p style="color:#888; font-size:12px; margin-bottom:4px;">
|
|
Warum möchtest du bei uns mitspielen? (mind. 20 Zeichen - ein Admin schaut sich das vor der Freischaltung an)
|
|
</p>
|
|
<textarea id="regApplicationText" placeholder="Kurz erzählen, wer du bist und warum du mitspielen möchtest..." style="width:100%; box-sizing:border-box; min-height:80px; resize:vertical;"></textarea>
|
|
<div class="form-row">
|
|
<button onclick="doRegister()">Account erstellen</button>
|
|
<button class="secondary" onclick="hideRegister()">Abbrechen</button>
|
|
</div>
|
|
<div class="msg" id="registerMsg"></div>
|
|
<p style="color:#888; font-size:13px; margin-bottom:0;">
|
|
Nach der Registrierung muss ein Admin deinen Account erst freischalten, bevor du dich einloggen kannst.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<main>
|
|
|
|
<div class="play-banner hidden" id="playBanner">
|
|
<div>
|
|
<h2>Bereit zum Spielen?</h2>
|
|
<p>Log dich ein und tauch direkt in die Welt ein.</p>
|
|
</div>
|
|
<button onclick="location.href='/game3d.html'">▶ Spiel starten</button>
|
|
</div>
|
|
|
|
<!-- NEWS + CHANGELOG nebeneinander (für alle sichtbar) -->
|
|
<div class="news-columns">
|
|
<section class="panel">
|
|
<h2>📰 News</h2>
|
|
<div id="newsList"></div>
|
|
<div id="newsEmpty" class="hidden">Noch keine News vorhanden.</div>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<h2>🛠️ Changelog</h2>
|
|
<div id="changelogList"></div>
|
|
<div id="changelogEmpty" class="hidden">Noch keine Einträge vorhanden.</div>
|
|
</section>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<footer style="max-width:900px; margin:0 auto 30px; padding:0 20px; color:#666; font-size:13px; display:flex; justify-content:space-between; flex-wrap:wrap; gap:10px;">
|
|
<span>Version <span id="footerVersion">-</span></span>
|
|
<span>
|
|
<a href="/impressum.html" style="color:#888; text-decoration:none;">Impressum</a> ·
|
|
<a href="/datenschutz.html" style="color:#888; text-decoration:none;">Datenschutz</a>
|
|
</span>
|
|
</footer>
|
|
|
|
<script>
|
|
let token = localStorage.getItem("token") || null;
|
|
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
|
let username = localStorage.getItem("username") || "";
|
|
|
|
function updateAuthUI() {
|
|
const loginArea = document.getElementById("loginArea");
|
|
const loggedInArea = document.getElementById("loggedInArea");
|
|
const playBanner = document.getElementById("playBanner");
|
|
|
|
if (token) {
|
|
loginArea.style.display = "none";
|
|
loggedInArea.style.display = "flex";
|
|
document.getElementById("welcomeText").textContent = "Angemeldet als " + username;
|
|
playBanner.classList.remove("hidden");
|
|
} else {
|
|
loginArea.style.display = "flex";
|
|
loggedInArea.style.display = "none";
|
|
playBanner.classList.add("hidden");
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// REGISTRIERUNG
|
|
// -------------------------------------------------------------
|
|
function showRegister() {
|
|
document.getElementById("registerOverlay").classList.remove("hidden");
|
|
}
|
|
|
|
function hideRegister() {
|
|
document.getElementById("registerOverlay").classList.add("hidden");
|
|
document.getElementById("regUser").value = "";
|
|
document.getElementById("regPass").value = "";
|
|
document.getElementById("regPassConfirm").value = "";
|
|
document.getElementById("regApplicationText").value = "";
|
|
document.getElementById("registerMsg").textContent = "";
|
|
}
|
|
|
|
async function doRegister() {
|
|
const username = document.getElementById("regUser").value.trim();
|
|
const password = document.getElementById("regPass").value;
|
|
const passwordConfirm = document.getElementById("regPassConfirm").value;
|
|
const applicationText = document.getElementById("regApplicationText").value.trim();
|
|
|
|
if (!username || !password) {
|
|
showRegisterMsg("Benutzername und Passwort sind Pflicht.", true);
|
|
return;
|
|
}
|
|
if (password !== passwordConfirm) {
|
|
showRegisterMsg("Passwörter stimmen nicht überein.", true);
|
|
return;
|
|
}
|
|
if (password.length < 4) {
|
|
showRegisterMsg("Passwort muss mind. 4 Zeichen haben.", true);
|
|
return;
|
|
}
|
|
if (applicationText.length < 20) {
|
|
showRegisterMsg("Bitte kurz begründen, warum du mitspielen möchtest (mind. 20 Zeichen).", true);
|
|
return;
|
|
}
|
|
|
|
const res = await fetch("/api/register", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ username, password, applicationText })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (!data.ok) {
|
|
showRegisterMsg(data.error || "Registrierung fehlgeschlagen.", true);
|
|
return;
|
|
}
|
|
|
|
showRegisterMsg("Account erstellt! Ein Admin muss dich noch freischalten.");
|
|
setTimeout(hideRegister, 2500);
|
|
}
|
|
|
|
function showRegisterMsg(text, isError) {
|
|
const el = document.getElementById("registerMsg");
|
|
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
|
el.textContent = text;
|
|
}
|
|
|
|
async function doLogin() {
|
|
const user = document.getElementById("loginUser").value;
|
|
const pass = document.getElementById("loginPass").value;
|
|
|
|
if (!user || !pass) return;
|
|
|
|
const res = await fetch("/api/login", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ username: user, password: pass })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (!data.ok) {
|
|
alert(data.error || "Login fehlgeschlagen");
|
|
return;
|
|
}
|
|
|
|
token = data.token;
|
|
isAdmin = !!data.isAdmin;
|
|
username = data.username;
|
|
|
|
localStorage.setItem("token", token);
|
|
localStorage.setItem("isAdmin", isAdmin ? "true" : "false");
|
|
localStorage.setItem("username", username);
|
|
|
|
updateAuthUI();
|
|
}
|
|
|
|
function doLogout() {
|
|
token = null;
|
|
isAdmin = false;
|
|
username = "";
|
|
localStorage.removeItem("token");
|
|
localStorage.removeItem("isAdmin");
|
|
localStorage.removeItem("username");
|
|
updateAuthUI();
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// NEWS
|
|
// -------------------------------------------------------------
|
|
async function loadNews() {
|
|
const res = await fetch("/api/news");
|
|
const data = await res.json();
|
|
renderNews(data.news || []);
|
|
}
|
|
|
|
async function loadChangelog() {
|
|
const res = await fetch("/api/changelog");
|
|
const data = await res.json();
|
|
renderChangelog(data.changelog || []);
|
|
}
|
|
|
|
function renderChangelog(items) {
|
|
const list = document.getElementById("changelogList");
|
|
const empty = document.getElementById("changelogEmpty");
|
|
list.innerHTML = "";
|
|
|
|
if (items.length === 0) {
|
|
empty.classList.remove("hidden");
|
|
return;
|
|
}
|
|
empty.classList.add("hidden");
|
|
|
|
items.forEach(c => {
|
|
const div = document.createElement("div");
|
|
div.className = "news-item";
|
|
|
|
const date = new Date(c.created_at).toLocaleString("de-DE");
|
|
|
|
div.innerHTML = `
|
|
<h3>${escapeHtml(c.title)} <span class="badge">v${escapeHtml(c.version)}</span></h3>
|
|
<div class="news-meta">${date}</div>
|
|
<div class="news-content">${escapeHtml(c.content)}</div>
|
|
`;
|
|
|
|
if (isAdmin) {
|
|
const delBtn = document.createElement("button");
|
|
delBtn.className = "danger";
|
|
delBtn.textContent = "Löschen";
|
|
delBtn.onclick = () => deleteChangelog(c.id);
|
|
div.appendChild(delBtn);
|
|
}
|
|
|
|
list.appendChild(div);
|
|
});
|
|
}
|
|
|
|
async function deleteChangelog(id) {
|
|
if (!confirm("Diesen Changelog-Eintrag wirklich löschen?")) return;
|
|
|
|
const res = await fetch("/api/admin/changelog/" + id, {
|
|
method: "DELETE",
|
|
headers: { "Authorization": "Bearer " + token }
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.ok) loadChangelog();
|
|
else alert("Löschen fehlgeschlagen.");
|
|
}
|
|
|
|
async function loadVersion() {
|
|
try {
|
|
const res = await fetch("/api/status");
|
|
const data = await res.json();
|
|
const el = document.getElementById("footerVersion");
|
|
if (el && data.version) el.textContent = data.version;
|
|
} catch {}
|
|
}
|
|
|
|
function renderNews(items) {
|
|
const list = document.getElementById("newsList");
|
|
const empty = document.getElementById("newsEmpty");
|
|
list.innerHTML = "";
|
|
|
|
if (items.length === 0) {
|
|
empty.classList.remove("hidden");
|
|
return;
|
|
}
|
|
empty.classList.add("hidden");
|
|
|
|
items.forEach(n => {
|
|
const div = document.createElement("div");
|
|
div.className = "news-item";
|
|
|
|
const date = new Date(n.created_at).toLocaleString("de-DE");
|
|
|
|
div.innerHTML = `
|
|
<h3>${escapeHtml(n.title)}</h3>
|
|
<div class="news-meta">${date}${n.author ? " — " + escapeHtml(n.author) : ""}</div>
|
|
<div class="news-content">${escapeHtml(n.content)}</div>
|
|
`;
|
|
|
|
if (isAdmin) {
|
|
const delBtn = document.createElement("button");
|
|
delBtn.className = "danger";
|
|
delBtn.textContent = "Löschen";
|
|
delBtn.onclick = () => deleteNews(n.id);
|
|
div.appendChild(delBtn);
|
|
}
|
|
|
|
list.appendChild(div);
|
|
});
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
const div = document.createElement("div");
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
async function deleteNews(id) {
|
|
if (!confirm("Diese News wirklich löschen?")) return;
|
|
|
|
const res = await fetch("/api/admin/news/" + id, {
|
|
method: "DELETE",
|
|
headers: { "Authorization": "Bearer " + token }
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.ok) {
|
|
loadNews();
|
|
} else {
|
|
alert("Löschen fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// ONLINE-ZÄHLER
|
|
// -------------------------------------------------------------
|
|
async function loadOnlineCount() {
|
|
try {
|
|
const res = await fetch("/api/status");
|
|
const data = await res.json();
|
|
const el = document.getElementById("onlineCount");
|
|
if (el && data.ok) el.textContent = `👥 ${data.online} online`;
|
|
} catch {
|
|
// Server evtl. gerade nicht erreichbar - still bleiben, kein Fehler-Popup nötig
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// SEITEN-TITEL (einstellbar über /admin_settings.html o.ä.)
|
|
// -------------------------------------------------------------
|
|
async function loadSiteTitle() {
|
|
try {
|
|
const res = await fetch("/api/site_config");
|
|
const data = await res.json();
|
|
if (data.ok && data.title) {
|
|
document.title = data.title;
|
|
const el = document.getElementById("siteTitleText");
|
|
if (el) el.textContent = data.title;
|
|
const input = document.getElementById("siteTitleInput");
|
|
if (input && !input.value) input.value = data.title;
|
|
}
|
|
} catch (err) {
|
|
console.error("Seiten-Titel konnte nicht geladen werden:", err);
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// INIT
|
|
// -------------------------------------------------------------
|
|
updateAuthUI();
|
|
loadSiteTitle();
|
|
loadNews();
|
|
loadChangelog();
|
|
loadVersion();
|
|
loadOnlineCount();
|
|
setInterval(loadOnlineCount, 10000);
|
|
|
|
// Hinweis anzeigen, falls man wegen Verbindungsverlust aus dem Spiel hierher geschickt wurde
|
|
if (new URLSearchParams(location.search).get("disconnected") === "1") {
|
|
alert("Die Verbindung zum Server wurde unterbrochen. Du wurdest zur Startseite zurückgebracht - logg dich einfach erneut ein.");
|
|
history.replaceState(null, "", location.pathname);
|
|
}
|
|
|
|
// Hinweis anzeigen, falls ein geplanter Server-Neustart die Verbindung beendet hat
|
|
if (new URLSearchParams(location.search).get("restarted") === "1") {
|
|
alert("Der Server wurde für einen Neustart neu gestartet. Warte einen Moment und logg dich dann erneut ein.");
|
|
history.replaceState(null, "", location.pathname);
|
|
}
|
|
|
|
// Hinweis anzeigen, falls der Zugang zum 3D-Spiel verweigert wurde (kein Beta-Tester)
|
|
if (new URLSearchParams(location.search).get("betaonly") === "1") {
|
|
alert("Das 3D-Spiel ist aktuell nur für Beta-Tester zugänglich. Wende dich an einen Admin, falls du Zugang haben möchtest.");
|
|
history.replaceState(null, "", location.pathname);
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html> |