Erster Commit
This commit is contained in:
+392
@@ -0,0 +1,392 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Mein Profil</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
main {
|
||||
max-width: 500px;
|
||||
margin: 20px auto 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.panel h2 {
|
||||
margin-top: 0;
|
||||
border-bottom: 2px solid #333;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #262626;
|
||||
font-size: 15px;
|
||||
}
|
||||
.stat-row:last-child { border-bottom: none; }
|
||||
.stat-row .label { color: #999; }
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
background: #2f2a5e;
|
||||
color: #b3a6ff;
|
||||
}
|
||||
.badge.online { background: #1a4d2a; color: #6fbf73; }
|
||||
.badge.offline { background: #333; color: #999; }
|
||||
|
||||
.skin-btn {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 24px;
|
||||
background: #222;
|
||||
border: 2px solid #444;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.skin-btn.selected { border-color: #2c7a3d; background: #1a4d2a; }
|
||||
.skin-btn:hover { border-color: #6fbf73; }
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #2c7a3d;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 14px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a>
|
||||
<h1>👤 Mein Profil</h1>
|
||||
|
||||
<main>
|
||||
<div class="panel">
|
||||
<h2 id="profileUsername">...</h2>
|
||||
|
||||
<div class="stat-row"><span class="label">Status</span> <span id="onlineBadge"></span></div>
|
||||
<div class="stat-row"><span class="label">Bargeld</span> <span id="statMoney"></span></div>
|
||||
<div class="stat-row"><span class="label">Bank</span> <span id="statBank"></span></div>
|
||||
<div class="stat-row"><span class="label">Leben</span> <span id="statHealth"></span></div>
|
||||
<div class="stat-row"><span class="label">Hunger</span> <span id="statHunger"></span></div>
|
||||
<div class="stat-row"><span class="label">Durst</span> <span id="statThirst"></span></div>
|
||||
<div class="stat-row"><span class="label">Aktuelle Welt</span> <span id="statWorld"></span></div>
|
||||
<div class="stat-row"><span class="label">Spielzeit</span> <span id="statPlaytime"></span></div>
|
||||
<div class="stat-row"><span class="label">Rolle</span> <span id="statRole"></span></div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Charakterfarbe</h2>
|
||||
<p style="color:#888; font-size:13px; margin-top:0;">
|
||||
So wirst du für andere Spieler im Spiel dargestellt.
|
||||
</p>
|
||||
<div style="display:flex; align-items:center; gap:12px;">
|
||||
<input type="color" id="colorPicker" value="#f1c40f" style="width:60px; height:40px; padding:2px; cursor:pointer;">
|
||||
<div id="colorPreview" style="width:32px; height:32px; border:2px solid #444; border-radius:4px; background:#f1c40f;"></div>
|
||||
<button onclick="saveColor()">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="colorMsg"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Skin</h2>
|
||||
<p style="color:#888; font-size:13px; margin-top:0;">
|
||||
Wird zusätzlich zur Farbe über deinem Charakter angezeigt.
|
||||
</p>
|
||||
<div id="skinGrid" style="display:flex; flex-wrap:wrap; gap:8px;"></div>
|
||||
<div class="msg" id="skinMsg"></div>
|
||||
|
||||
<h3 style="margin-top:20px; font-size:14px; color:#f5d90a;">🎨 Gemalte Skins</h3>
|
||||
<p style="color:#888; font-size:13px; margin-top:0;">
|
||||
Ersetzt das Emoji durch ein selbst gemaltes Bild über dem ganzen Körper.
|
||||
</p>
|
||||
<div id="paintedSkinGrid" style="display:flex; flex-wrap:wrap; gap:8px;"></div>
|
||||
<div class="msg" id="paintedSkinMsg"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Passwort ändern</h2>
|
||||
<input type="password" id="currentPassword" placeholder="Aktuelles Passwort">
|
||||
<input type="password" id="newPassword" placeholder="Neues Passwort">
|
||||
<button onclick="changePassword()">Passwort ändern</button>
|
||||
<div class="msg" id="pwMsg"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
if (!token) {
|
||||
alert("Bitte zuerst auf der Startseite einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401) {
|
||||
alert("Sitzung abgelaufen. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
const res = await authFetch("/api/me");
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.ok) {
|
||||
alert("Profil konnte nicht geladen werden.");
|
||||
return;
|
||||
}
|
||||
|
||||
const p = data.player;
|
||||
document.getElementById("profileUsername").textContent = p.username;
|
||||
document.getElementById("onlineBadge").innerHTML = p.online
|
||||
? `<span class="badge online">Online</span>`
|
||||
: `<span class="badge offline">Offline</span>`;
|
||||
document.getElementById("statMoney").textContent = p.money + "$";
|
||||
document.getElementById("statBank").textContent = p.bank + "$";
|
||||
document.getElementById("statHealth").textContent = Math.round(p.health);
|
||||
document.getElementById("statHunger").textContent = Math.round(p.hunger);
|
||||
document.getElementById("statThirst").textContent = Math.round(p.thirst);
|
||||
document.getElementById("statWorld").textContent = p.world;
|
||||
|
||||
const playtimeSec = p.playtimeSeconds || 0;
|
||||
const ph = Math.floor(playtimeSec / 3600);
|
||||
const pm = Math.floor((playtimeSec % 3600) / 60);
|
||||
document.getElementById("statPlaytime").textContent = `${ph}h ${pm}min`;
|
||||
|
||||
document.getElementById("statRole").innerHTML = p.isAdmin
|
||||
? `<span class="badge">Admin</span>`
|
||||
: "Spieler";
|
||||
|
||||
const color = p.color || "#f1c40f";
|
||||
document.getElementById("colorPicker").value = color;
|
||||
document.getElementById("colorPreview").style.background = color;
|
||||
|
||||
currentSkin = p.skin || "none";
|
||||
renderSkinGrid();
|
||||
|
||||
currentSkinItemId = p.skinItemId || null;
|
||||
loadPaintedSkins();
|
||||
}
|
||||
|
||||
document.getElementById("colorPicker").addEventListener("input", e => {
|
||||
document.getElementById("colorPreview").style.background = e.target.value;
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// SKIN
|
||||
// -------------------------------------------------------------
|
||||
let allSkins = ["none"];
|
||||
let currentSkin = "none";
|
||||
|
||||
async function loadSkins() {
|
||||
const res = await fetch("/api/skins");
|
||||
const data = await res.json();
|
||||
allSkins = data.skins || ["none"];
|
||||
renderSkinGrid();
|
||||
}
|
||||
|
||||
function renderSkinGrid() {
|
||||
const grid = document.getElementById("skinGrid");
|
||||
grid.innerHTML = "";
|
||||
|
||||
allSkins.forEach(skin => {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "skin-btn" + (skin === currentSkin ? " selected" : "");
|
||||
btn.textContent = skin === "none" ? "❌" : skin;
|
||||
btn.title = skin === "none" ? "Kein Skin" : skin;
|
||||
btn.onclick = () => saveSkin(skin);
|
||||
grid.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
async function saveSkin(skin) {
|
||||
const res = await authFetch("/api/me/skin", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ skin })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
currentSkin = skin;
|
||||
renderSkinGrid();
|
||||
showSkinMsg("Skin gespeichert.");
|
||||
} else {
|
||||
showSkinMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// GEMALTE SKINS (Katalog, ersetzt das Emoji)
|
||||
// -------------------------------------------------------------
|
||||
let allPaintedSkins = [];
|
||||
let currentSkinItemId = null;
|
||||
|
||||
async function loadPaintedSkins() {
|
||||
const res = await fetch("/api/skin_catalog");
|
||||
const data = await res.json();
|
||||
allPaintedSkins = data.items || [];
|
||||
renderPaintedSkinGrid();
|
||||
}
|
||||
|
||||
function renderPaintedSkinGrid() {
|
||||
const grid = document.getElementById("paintedSkinGrid");
|
||||
grid.innerHTML = "";
|
||||
|
||||
const noneBtn = document.createElement("button");
|
||||
noneBtn.className = "skin-btn" + (currentSkinItemId === null ? " selected" : "");
|
||||
noneBtn.textContent = "❌";
|
||||
noneBtn.title = "Keinen gemalten Skin nutzen (zurück zum Emoji)";
|
||||
noneBtn.onclick = () => savePaintedSkin(null);
|
||||
grid.appendChild(noneBtn);
|
||||
|
||||
allPaintedSkins.forEach(item => {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "skin-btn" + (item.id === currentSkinItemId ? " selected" : "");
|
||||
btn.style.backgroundImage = `url(${item.image})`;
|
||||
btn.style.backgroundSize = "cover";
|
||||
btn.style.imageRendering = "pixelated";
|
||||
btn.title = item.name;
|
||||
btn.onclick = () => savePaintedSkin(item.id);
|
||||
grid.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
async function savePaintedSkin(skinId) {
|
||||
const res = await authFetch("/api/me/skin_item", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ skinId })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
currentSkinItemId = skinId;
|
||||
renderPaintedSkinGrid();
|
||||
showPaintedSkinMsg("Gespeichert.");
|
||||
} else {
|
||||
showPaintedSkinMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
function showPaintedSkinMsg(text, isError) {
|
||||
const el = document.getElementById("paintedSkinMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
function showSkinMsg(text, isError) {
|
||||
const el = document.getElementById("skinMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
async function saveColor() {
|
||||
const color = document.getElementById("colorPicker").value;
|
||||
|
||||
const res = await authFetch("/api/me/color", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ color })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showColorMsg("Farbe gespeichert.");
|
||||
} else {
|
||||
showColorMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
function showColorMsg(text, isError) {
|
||||
const el = document.getElementById("colorMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
async function changePassword() {
|
||||
const currentPassword = document.getElementById("currentPassword").value;
|
||||
const newPassword = document.getElementById("newPassword").value;
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
showMsg("Bitte beide Felder ausfüllen.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch("/api/me/password", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ currentPassword, newPassword })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Passwort geändert.");
|
||||
document.getElementById("currentPassword").value = "";
|
||||
document.getElementById("newPassword").value = "";
|
||||
} else {
|
||||
showMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
function showMsg(text, isError) {
|
||||
const el = document.getElementById("pwMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
loadSkins();
|
||||
loadProfile();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user