Erster Commit
This commit is contained in:
+589
@@ -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>
|
||||
Reference in New Issue
Block a user