Erster Commit

This commit is contained in:
2026-08-22 08:40:29 +02:00
commit 875477d425
1961 changed files with 930336 additions and 0 deletions
+589
View File
@@ -0,0 +1,589 @@
<!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>