This commit is contained in:
2026-02-03 09:18:15 +01:00
parent 13efe9406c
commit dc427490a5
42 changed files with 5104 additions and 65 deletions

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<linearGradient id="bg1" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#28f0ff"/>
<stop offset="1" stop-color="#ff3df2"/>
</linearGradient>
</defs>
<rect width="128" height="128" rx="20" fill="url(#bg1)"/>
<circle cx="64" cy="54" r="26" fill="rgba(0,0,0,0.35)"/>
<path d="M28 108c8-18 24-28 36-28h0c12 0 28 10 36 28" fill="rgba(0,0,0,0.35)"/>
<circle cx="64" cy="54" r="22" fill="rgba(255,255,255,0.85)"/>
<rect x="48" y="42" width="32" height="8" rx="4" fill="#0d1b26"/>
<rect x="52" y="58" width="24" height="6" rx="3" fill="#0d1b26"/>
</svg>

After

Width:  |  Height:  |  Size: 663 B

View File

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<linearGradient id="bg2" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#ffb347"/>
<stop offset="1" stop-color="#ff5f6d"/>
</linearGradient>
</defs>
<rect width="128" height="128" rx="20" fill="url(#bg2)"/>
<circle cx="64" cy="52" r="26" fill="rgba(0,0,0,0.3)"/>
<path d="M24 108c6-16 22-26 40-26h0c18 0 34 10 40 26" fill="rgba(0,0,0,0.3)"/>
<circle cx="64" cy="52" r="22" fill="rgba(255,255,255,0.86)"/>
<path d="M44 56h40" stroke="#2a0d0d" stroke-width="6" stroke-linecap="round"/>
<circle cx="54" cy="48" r="5" fill="#2a0d0d"/>
<circle cx="74" cy="48" r="5" fill="#2a0d0d"/>
</svg>

After

Width:  |  Height:  |  Size: 704 B

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<linearGradient id="bg3" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#3dffb5"/>
<stop offset="1" stop-color="#2b8cff"/>
</linearGradient>
</defs>
<rect width="128" height="128" rx="20" fill="url(#bg3)"/>
<circle cx="64" cy="50" r="26" fill="rgba(0,0,0,0.28)"/>
<path d="M22 108c10-18 26-28 42-28h0c16 0 32 10 42 28" fill="rgba(0,0,0,0.28)"/>
<circle cx="64" cy="50" r="22" fill="rgba(255,255,255,0.88)"/>
<rect x="46" y="44" width="36" height="10" rx="5" fill="#092022"/>
<rect x="48" y="60" width="32" height="6" rx="3" fill="#092022"/>
</svg>

After

Width:  |  Height:  |  Size: 665 B

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<linearGradient id="bg4" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#6dd5fa"/>
<stop offset="1" stop-color="#5b8cff"/>
</linearGradient>
</defs>
<rect width="128" height="128" rx="20" fill="url(#bg4)"/>
<circle cx="64" cy="52" r="26" fill="rgba(0,0,0,0.3)"/>
<path d="M26 108c8-18 24-28 38-28h0c14 0 30 10 38 28" fill="rgba(0,0,0,0.3)"/>
<circle cx="64" cy="52" r="22" fill="rgba(255,255,255,0.86)"/>
<path d="M46 56h36" stroke="#0b1b2e" stroke-width="6" stroke-linecap="round"/>
<path d="M52 44h24" stroke="#0b1b2e" stroke-width="6" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 687 B

View File

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<linearGradient id="bg5" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#ff6b6b"/>
<stop offset="1" stop-color="#f7b733"/>
</linearGradient>
</defs>
<rect width="128" height="128" rx="20" fill="url(#bg5)"/>
<circle cx="64" cy="50" r="26" fill="rgba(0,0,0,0.32)"/>
<path d="M22 108c10-16 26-26 42-26h0c16 0 32 10 42 26" fill="rgba(0,0,0,0.32)"/>
<circle cx="64" cy="50" r="22" fill="rgba(255,255,255,0.86)"/>
<circle cx="54" cy="50" r="5" fill="#2b0d0d"/>
<circle cx="74" cy="50" r="5" fill="#2b0d0d"/>
<rect x="50" y="62" width="28" height="6" rx="3" fill="#2b0d0d"/>
</svg>

After

Width:  |  Height:  |  Size: 694 B

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<linearGradient id="bg6" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#00f5d4"/>
<stop offset="1" stop-color="#00b4d8"/>
</linearGradient>
</defs>
<rect width="128" height="128" rx="20" fill="url(#bg6)"/>
<circle cx="64" cy="52" r="26" fill="rgba(0,0,0,0.28)"/>
<path d="M24 108c8-18 24-28 40-28h0c16 0 32 10 40 28" fill="rgba(0,0,0,0.28)"/>
<circle cx="64" cy="52" r="22" fill="rgba(255,255,255,0.88)"/>
<rect x="46" y="46" width="36" height="8" rx="4" fill="#072127"/>
<path d="M52 60h24" stroke="#072127" stroke-width="6" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 676 B

View File

@@ -416,6 +416,53 @@ button:active, .btn:active, input[type="submit"]:active{ transform: translateY(0
.actions{ display:flex; gap: 12px; margin-top: 14px; flex-wrap: wrap; }
/* Auth */
.auth-view{ display:flex; flex-direction:column; gap: 16px; }
.auth-card{ max-width: 720px; margin: 0 auto; width: 100%; }
.form-row{ margin-top: 12px; }
.input{
width: 100%;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(145,220,255,.22);
background: rgba(0,0,0,.28);
color: var(--text);
}
.input:focus{ outline: 2px solid rgba(66,245,255,.35); }
.auth-grid{
display:grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
margin-top: 12px;
}
.auth-choice{
border: 1px solid rgba(145,220,255,.18);
background: rgba(0,0,0,.18);
border-radius: 14px;
padding: 12px;
color: var(--text);
text-align: left;
display:flex;
flex-direction:column;
gap: 6px;
cursor:pointer;
}
.auth-choice:hover{ border-color: rgba(66,245,255,.45); box-shadow: var(--glow-cyan); transform: translateY(-1px); }
.auth-choice.is-selected{
border-color: rgba(66,245,255,.55);
box-shadow: var(--glow-cyan);
background: linear-gradient(180deg, rgba(66,245,255,.16), rgba(91,124,255,.10));
}
.auth-choice-title{ font-family: var(--font-sci); letter-spacing: .6px; }
.auth-choice-meta{ color: var(--muted); font-size: .85rem; }
.auth-message{ min-height: 18px; margin-top: 8px; }
.auth-avatar{
width: 100%;
border-radius: 12px;
border: 1px solid rgba(145,220,255,.18);
background: rgba(0,0,0,.22);
}
/* Resourcebar sticky */
.resourcebar{
position: sticky;

View File

@@ -119,28 +119,31 @@
});
});
const authView = document.getElementById("authView");
const gameView = document.getElementById("gameView");
let stateTimer = null;
const authPanels = {
login: document.getElementById("authLogin"),
"register-step1": document.getElementById("authRegisterStep1"),
"register-step2": document.getElementById("authRegisterStep2"),
"register-step3": document.getElementById("authRegisterStep3")
};
let racesCache = null;
let avatarsCache = null;
const regDraft = { race_key: null, avatar_key: null };
function formatNumber(val){
const num = Number.isFinite(val) ? val : 0;
return Math.round(num).toLocaleString("de-DE");
}
const resourceMap = {
"Metall": "metal",
"Kristall": "crystals",
"Deuterium": "deuterium",
"Energie": "energy"
};
function updateResourceBar(state){
const stats = document.querySelectorAll(".resource-row .stat");
stats.forEach((stat)=>{
const label = stat.querySelector(".stat-k")?.textContent?.trim();
const key = resourceMap[label];
const values = state?.resources || {};
document.querySelectorAll("[data-resource-value]").forEach((valueEl)=>{
const key = valueEl.dataset.resourceValue;
if(!key) return;
const value = state?.resources?.[key];
const value = values[key];
if(typeof value !== "number") return;
const valueEl = stat.querySelector(".stat-v");
if(!valueEl) return;
const dot = valueEl.querySelector(".dot");
const display = key === "energy" ? Math.round(value) : Math.floor(value);
const prefix = key === "energy" && display >= 0 ? "+" : "";
@@ -150,19 +153,189 @@
});
}
function updateQueuePanel(state){
const slotsEl = document.getElementById("queueSlots");
if(slotsEl){
const slots = Number.isFinite(state?.queue_slots) ? state.queue_slots : null;
slotsEl.textContent = slots === null ? "" : String(slots);
}
const list = document.getElementById("queueList");
if(!list) return;
list.textContent = "";
const jobs = Array.isArray(state?.active_build_jobs) ? state.active_build_jobs : [];
if(jobs.length === 0){
const li = document.createElement("li");
li.className = "muted";
li.textContent = "Keine aktiven Baujobs.";
list.appendChild(li);
return;
}
jobs.forEach((job)=>{
const li = document.createElement("li");
const slotIndex = Number.isFinite(Number(job?.slot_index)) ? Number(job.slot_index) + 1 : null;
const slotLabel = slotIndex ? `Slot ${slotIndex}: ` : "";
const key = job?.building_key ? String(job.building_key) : "Unbekannt";
const finish = job?.finish_at ? ` · fertig ${job.finish_at}` : "";
li.textContent = `${slotLabel}${key}${finish}`;
list.appendChild(li);
});
}
function showAuthView(){
if(authView) authView.removeAttribute("hidden");
if(gameView) gameView.setAttribute("hidden", "");
}
function showGameView(){
if(authView) authView.setAttribute("hidden", "");
if(gameView) gameView.removeAttribute("hidden");
}
function showAuthPanel(key){
Object.values(authPanels).forEach((panel)=>{
if(panel) panel.setAttribute("hidden", "");
});
const panel = authPanels[key];
if(panel) panel.removeAttribute("hidden");
}
function setMessage(el, message){
if(!el) return;
el.textContent = message || "";
}
async function fetchJson(url, options){
try{
const res = await fetch(url, options);
const data = await res.json().catch(()=> ({}));
return { ok: res.ok, status: res.status, data };
}catch(e){
return { ok: false, status: 0, data: {} };
}
}
async function loadRaces(){
if(racesCache) return racesCache;
const result = await fetchJson("/api/meta/races");
if(result.ok){
racesCache = result.data.races || [];
}else{
racesCache = [];
}
return racesCache;
}
async function loadAvatars(){
if(avatarsCache) return avatarsCache;
const result = await fetchJson("/api/meta/avatars");
if(result.ok){
avatarsCache = result.data.avatars || [];
}else{
avatarsCache = [];
}
return avatarsCache;
}
function renderRaces(races){
const list = document.getElementById("raceList");
if(!list) return;
list.textContent = "";
if(!Array.isArray(races) || races.length === 0){
const empty = document.createElement("div");
empty.className = "muted";
empty.textContent = "Keine Rassen verfügbar.";
list.appendChild(empty);
return;
}
races.forEach((race)=>{
const btn = document.createElement("button");
btn.type = "button";
btn.className = "auth-choice";
btn.dataset.raceKey = race.key;
if(regDraft.race_key === race.key) btn.classList.add("is-selected");
const summary = Array.isArray(race.modifier_summary) ? race.modifier_summary : [];
btn.innerHTML = `
<div class="auth-choice-title">${escapeHtml(race.name || race.key)}</div>
<div class="auth-choice-meta">${escapeHtml(race.description || "")}</div>
${summary.length ? `<div class="auth-choice-meta">${summary.map(s=>escapeHtml(s)).join("<br>")}</div>` : ""}
`;
btn.addEventListener("click", ()=>{
regDraft.race_key = race.key;
list.querySelectorAll(".auth-choice").forEach(el=>el.classList.remove("is-selected"));
btn.classList.add("is-selected");
});
list.appendChild(btn);
});
}
function renderAvatars(avatars){
const list = document.getElementById("avatarList");
if(!list) return;
list.textContent = "";
if(!Array.isArray(avatars) || avatars.length === 0){
const empty = document.createElement("div");
empty.className = "muted";
empty.textContent = "Keine Avatare verfügbar.";
list.appendChild(empty);
return;
}
avatars.forEach((avatar)=>{
const btn = document.createElement("button");
btn.type = "button";
btn.className = "auth-choice";
btn.dataset.avatarKey = avatar.key;
if(regDraft.avatar_key === avatar.key) btn.classList.add("is-selected");
btn.innerHTML = `
<img class="auth-avatar" src="${escapeHtml(avatar.image || "")}" alt="${escapeHtml(avatar.label || avatar.key)}">
<div class="auth-choice-title">${escapeHtml(avatar.label || avatar.key)}</div>
`;
btn.addEventListener("click", ()=>{
regDraft.avatar_key = avatar.key;
list.querySelectorAll(".auth-choice").forEach(el=>el.classList.remove("is-selected"));
btn.classList.add("is-selected");
});
list.appendChild(btn);
});
}
function startPolling(){
if(stateTimer) return;
stateTimer = setInterval(refreshState, 15000);
}
function stopPolling(){
if(!stateTimer) return;
clearInterval(stateTimer);
stateTimer = null;
}
async function fetchState(){
try{
const res = await fetch("/api/state");
if(!res.ok) return null;
return await res.json();
const res = await fetch("/api/state", { credentials: "same-origin" });
if(res.status === 401) return { status: 401 };
if(!res.ok) return { status: res.status };
const data = await res.json();
return { status: 200, data };
}catch(e){
return null;
return { status: 0 };
}
}
async function refreshState(){
const state = await fetchState();
if(state) updateResourceBar(state);
const result = await fetchState();
if(result.status === 200){
showGameView();
updateResourceBar(result.data);
updateQueuePanel(result.data);
ensureBuildButton();
startPolling();
return;
}
if(result.status === 401){
showAuthView();
showAuthPanel("login");
stopPolling();
}
}
function ensureBuildButton(){
@@ -198,8 +371,130 @@
}
document.addEventListener("DOMContentLoaded", ()=>{
document.querySelectorAll("[data-auth-switch]").forEach((btn)=>{
btn.addEventListener("click", async ()=>{
const target = btn.dataset.authSwitch;
if(!target) return;
showAuthView();
showAuthPanel(target);
if(target === "register-step1"){
const races = await loadRaces();
renderRaces(races);
}
if(target === "register-step2"){
const avatars = await loadAvatars();
renderAvatars(avatars);
}
});
});
const loginForm = document.getElementById("loginForm");
if(loginForm){
loginForm.addEventListener("submit", async (e)=>{
e.preventDefault();
const identifier = document.getElementById("loginIdentifier")?.value?.trim() || "";
const password = document.getElementById("loginPassword")?.value || "";
const message = document.getElementById("loginMessage");
setMessage(message, "");
const result = await fetchJson("/api/auth/login", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ username_or_email: identifier, password })
});
if(result.ok){
showGameView();
refreshState();
}else{
setMessage(message, result.data?.message || "Login fehlgeschlagen.");
}
});
}
const raceNext = document.getElementById("raceNext");
if(raceNext){
raceNext.addEventListener("click", async ()=>{
const message = document.getElementById("raceMessage");
setMessage(message, "");
if(!regDraft.race_key){
setMessage(message, "Bitte eine Rasse wählen.");
return;
}
const result = await fetchJson("/api/auth/register/step1", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ race_key: regDraft.race_key })
});
if(result.ok){
showAuthPanel("register-step2");
const avatars = await loadAvatars();
renderAvatars(avatars);
}else{
setMessage(message, result.data?.message || "Schritt 1 fehlgeschlagen.");
}
});
}
const avatarNext = document.getElementById("avatarNext");
if(avatarNext){
avatarNext.addEventListener("click", async ()=>{
const title = document.getElementById("regTitle")?.value?.trim() || "";
const message = document.getElementById("avatarMessage");
setMessage(message, "");
if(!regDraft.avatar_key){
setMessage(message, "Bitte einen Avatar wählen.");
return;
}
if(title.length < 2){
setMessage(message, "Titel ist zu kurz.");
return;
}
const result = await fetchJson("/api/auth/register/step2", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ avatar_key: regDraft.avatar_key, title })
});
if(result.ok){
showAuthPanel("register-step3");
}else{
setMessage(message, result.data?.message || "Schritt 2 fehlgeschlagen.");
}
});
}
const registerForm = document.getElementById("registerForm");
if(registerForm){
registerForm.addEventListener("submit", async (e)=>{
e.preventDefault();
const username = document.getElementById("regUsername")?.value?.trim() || "";
const email = document.getElementById("regEmail")?.value?.trim() || "";
const password = document.getElementById("regPassword")?.value || "";
const message = document.getElementById("registerMessage");
setMessage(message, "");
const result = await fetchJson("/api/auth/register/step3", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ username, email, password })
});
if(result.ok){
showGameView();
refreshState();
}else{
setMessage(message, result.data?.message || "Registrierung fehlgeschlagen.");
}
});
}
const logoutBtn = document.getElementById("logoutBtn");
if(logoutBtn){
logoutBtn.addEventListener("click", async ()=>{
await fetchJson("/api/auth/logout", { method: "POST" });
stopPolling();
showAuthView();
showAuthPanel("login");
});
}
showAuthPanel("login");
refreshState();
ensureBuildButton();
setInterval(refreshState, 30000);
});
})();

View File

@@ -103,7 +103,14 @@ $partialsPath = __DIR__ . '/../src/partials';
<div class="space-bg" aria-hidden="true"></div>
<div class="container">
<div class="app">
<div class="auth-view" id="authView" hidden>
<?php include $partialsPath . '/auth-login.php'; ?>
<?php include $partialsPath . '/auth-register-step1.php'; ?>
<?php include $partialsPath . '/auth-register-step2.php'; ?>
<?php include $partialsPath . '/auth-register-step3.php'; ?>
</div>
<div class="app" id="gameView">
<!-- LINKS: Sidebar -->
<aside class="sidebar" aria-label="Seitenleiste">
@@ -132,6 +139,7 @@ $partialsPath = __DIR__ . '/../src/partials';
🔔 <span class="badge" id="notifBadge">3</span>
</button>
<button class="btn btn-primary" type="button" onclick="toast('success','Beacon','Signal empfangen ✅')">Test Toast</button>
<button class="btn" type="button" id="logoutBtn">Logout</button>
</div>
</div>
@@ -211,6 +219,14 @@ $partialsPath = __DIR__ . '/../src/partials';
</div>
<?php include $partialsPath . '/site.php'; ?>
<section class="card inner panel" id="queuePanel" style="margin-top:14px;">
<h2 class="h2">Bauqueue (Live)</h2>
<div class="muted">Slots: <span id="queueSlots">0</span></div>
<ul id="queueList">
<li class="muted">Keine aktiven Baujobs.</li>
</ul>
</section>
</main>
<!-- Footer -->

View File

@@ -0,0 +1,23 @@
<section class="card panel auth-card" id="authLogin">
<div class="panel-title">LOGIN</div>
<p class="muted">Melde dich an, um deine Kolonie zu laden.</p>
<form id="loginForm">
<div class="form-row">
<label class="label" for="loginIdentifier">Username oder E-Mail</label>
<input class="input" id="loginIdentifier" name="username_or_email" type="text" autocomplete="username" required>
</div>
<div class="form-row">
<label class="label" for="loginPassword">Passwort</label>
<input class="input" id="loginPassword" name="password" type="password" autocomplete="current-password" required>
</div>
<div class="actions">
<button class="btn btn-primary" type="submit">Login</button>
<button class="btn" type="button" data-auth-switch="register-step1">Registrieren</button>
</div>
<div class="auth-message muted tiny" id="loginMessage" aria-live="polite"></div>
</form>
</section>

View File

@@ -0,0 +1,16 @@
<section class="card panel auth-card" id="authRegisterStep1" hidden>
<div class="panel-title">REGISTRIERUNG 1/3</div>
<h2 class="h2">Wähle deine Rasse</h2>
<p class="muted">Rasse bestimmt Startboni. Du kannst später über Forschungen nachsteuern.</p>
<div class="auth-grid" id="raceList">
<div class="muted">Lade Rassen ...</div>
</div>
<div class="actions">
<button class="btn" type="button" data-auth-switch="login">Zurück</button>
<button class="btn btn-primary" type="button" id="raceNext">Weiter</button>
</div>
<div class="auth-message muted tiny" id="raceMessage" aria-live="polite"></div>
</section>

View File

@@ -0,0 +1,22 @@
<section class="card panel auth-card" id="authRegisterStep2" hidden>
<div class="panel-title">REGISTRIERUNG 2/3</div>
<h2 class="h2">Avatar & Titel</h2>
<p class="muted">Wähle einen Avatar und gib deinem Captain einen Titel.</p>
<div class="auth-grid avatar-grid" id="avatarList">
<div class="muted">Lade Avatare ...</div>
</div>
<div class="form-row">
<label class="label" for="regTitle">Titel</label>
<input class="input" id="regTitle" name="title" type="text" maxlength="40" placeholder="z.B. Pionier, Architekt, Navigator" required>
<div class="muted tiny">240 Zeichen.</div>
</div>
<div class="actions">
<button class="btn" type="button" data-auth-switch="register-step1">Zurück</button>
<button class="btn btn-primary" type="button" id="avatarNext">Weiter</button>
</div>
<div class="auth-message muted tiny" id="avatarMessage" aria-live="polite"></div>
</section>

View File

@@ -0,0 +1,30 @@
<section class="card panel auth-card" id="authRegisterStep3" hidden>
<div class="panel-title">REGISTRIERUNG 3/3</div>
<h2 class="h2">Account erstellen</h2>
<p class="muted">Wähle deinen Accountnamen und sichere dein Profil.</p>
<form id="registerForm">
<div class="form-row">
<label class="label" for="regUsername">Username</label>
<input class="input" id="regUsername" name="username" type="text" autocomplete="username" required>
</div>
<div class="form-row">
<label class="label" for="regEmail">E-Mail</label>
<input class="input" id="regEmail" name="email" type="email" autocomplete="email" required>
</div>
<div class="form-row">
<label class="label" for="regPassword">Passwort</label>
<input class="input" id="regPassword" name="password" type="password" autocomplete="new-password" required>
<div class="muted tiny">Mindestens 8 Zeichen.</div>
</div>
<div class="actions">
<button class="btn" type="button" data-auth-switch="register-step2">Zurück</button>
<button class="btn btn-primary" type="submit">Account erstellen</button>
</div>
<div class="auth-message muted tiny" id="registerMessage" aria-live="polite"></div>
</form>
</section>

View File

@@ -5,24 +5,49 @@
</div>
<div class="stats">
<div class="stat">
<div class="stat" data-resource="metal">
<div class="stat-k">Metall</div>
<div class="stat-v"><span class="dot dot-cyan"></span> 12.340</div>
<div class="stat-v" id="res-metal" data-resource-value="metal"><span class="dot dot-cyan"></span> 12.340</div>
<div class="stat-bar"><span style="width:72%"></span></div>
</div>
<div class="stat">
<div class="stat" data-resource="crystals">
<div class="stat-k">Kristall</div>
<div class="stat-v"><span class="dot dot-pink"></span> 6.120</div>
<div class="stat-v" id="res-crystals" data-resource-value="crystals"><span class="dot dot-pink"></span> 6.120</div>
<div class="stat-bar"><span style="width:44%"></span></div>
</div>
<div class="stat">
<div class="stat" data-resource="deuterium">
<div class="stat-k">Deuterium</div>
<div class="stat-v"><span class="dot dot-green"></span> 3.880</div>
<div class="stat-v" id="res-deuterium" data-resource-value="deuterium"><span class="dot dot-green"></span> 3.880</div>
<div class="stat-bar"><span style="width:18%"></span></div>
</div>
<div class="stat">
<div class="stat" data-resource="energy">
<div class="stat-k">Energie</div>
<div class="stat-v"><span class="dot dot-warn"></span> +120</div>
<div class="stat-v" id="res-energy" data-resource-value="energy"><span class="dot dot-warn"></span> +120</div>
<div class="stat-bar"><span style="width:60%"></span></div>
</div>
<div class="stat" data-resource="alloy">
<div class="stat-k">Legierung</div>
<div class="stat-v" id="res-alloy" data-resource-value="alloy"><span class="dot dot-cyan"></span> 0</div>
<div class="stat-bar"><span style="width:40%"></span></div>
</div>
<div class="stat" data-resource="credits">
<div class="stat-k">Credits</div>
<div class="stat-v" id="res-credits" data-resource-value="credits"><span class="dot dot-pink"></span> 0</div>
<div class="stat-bar"><span style="width:35%"></span></div>
</div>
<div class="stat" data-resource="population">
<div class="stat-k">Bevölkerung</div>
<div class="stat-v" id="res-population" data-resource-value="population"><span class="dot dot-green"></span> 0</div>
<div class="stat-bar"><span style="width:55%"></span></div>
</div>
<div class="stat" data-resource="water">
<div class="stat-k">Wasser</div>
<div class="stat-v" id="res-water" data-resource-value="water"><span class="dot dot-cyan"></span> 0</div>
<div class="stat-bar"><span style="width:30%"></span></div>
</div>
<div class="stat" data-resource="food">
<div class="stat-k">Nahrung</div>
<div class="stat-v" id="res-food" data-resource-value="food"><span class="dot dot-green"></span> 0</div>
<div class="stat-bar"><span style="width:60%"></span></div>
</div>
</div>

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<linearGradient id="bg1" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#28f0ff"/>
<stop offset="1" stop-color="#ff3df2"/>
</linearGradient>
</defs>
<rect width="128" height="128" rx="20" fill="url(#bg1)"/>
<circle cx="64" cy="54" r="26" fill="rgba(0,0,0,0.35)"/>
<path d="M28 108c8-18 24-28 36-28h0c12 0 28 10 36 28" fill="rgba(0,0,0,0.35)"/>
<circle cx="64" cy="54" r="22" fill="rgba(255,255,255,0.85)"/>
<rect x="48" y="42" width="32" height="8" rx="4" fill="#0d1b26"/>
<rect x="52" y="58" width="24" height="6" rx="3" fill="#0d1b26"/>
</svg>

After

Width:  |  Height:  |  Size: 663 B

View File

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<linearGradient id="bg2" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#ffb347"/>
<stop offset="1" stop-color="#ff5f6d"/>
</linearGradient>
</defs>
<rect width="128" height="128" rx="20" fill="url(#bg2)"/>
<circle cx="64" cy="52" r="26" fill="rgba(0,0,0,0.3)"/>
<path d="M24 108c6-16 22-26 40-26h0c18 0 34 10 40 26" fill="rgba(0,0,0,0.3)"/>
<circle cx="64" cy="52" r="22" fill="rgba(255,255,255,0.86)"/>
<path d="M44 56h40" stroke="#2a0d0d" stroke-width="6" stroke-linecap="round"/>
<circle cx="54" cy="48" r="5" fill="#2a0d0d"/>
<circle cx="74" cy="48" r="5" fill="#2a0d0d"/>
</svg>

After

Width:  |  Height:  |  Size: 704 B

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<linearGradient id="bg3" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#3dffb5"/>
<stop offset="1" stop-color="#2b8cff"/>
</linearGradient>
</defs>
<rect width="128" height="128" rx="20" fill="url(#bg3)"/>
<circle cx="64" cy="50" r="26" fill="rgba(0,0,0,0.28)"/>
<path d="M22 108c10-18 26-28 42-28h0c16 0 32 10 42 28" fill="rgba(0,0,0,0.28)"/>
<circle cx="64" cy="50" r="22" fill="rgba(255,255,255,0.88)"/>
<rect x="46" y="44" width="36" height="10" rx="5" fill="#092022"/>
<rect x="48" y="60" width="32" height="6" rx="3" fill="#092022"/>
</svg>

After

Width:  |  Height:  |  Size: 665 B

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<linearGradient id="bg4" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#6dd5fa"/>
<stop offset="1" stop-color="#5b8cff"/>
</linearGradient>
</defs>
<rect width="128" height="128" rx="20" fill="url(#bg4)"/>
<circle cx="64" cy="52" r="26" fill="rgba(0,0,0,0.3)"/>
<path d="M26 108c8-18 24-28 38-28h0c14 0 30 10 38 28" fill="rgba(0,0,0,0.3)"/>
<circle cx="64" cy="52" r="22" fill="rgba(255,255,255,0.86)"/>
<path d="M46 56h36" stroke="#0b1b2e" stroke-width="6" stroke-linecap="round"/>
<path d="M52 44h24" stroke="#0b1b2e" stroke-width="6" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 687 B

View File

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<linearGradient id="bg5" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#ff6b6b"/>
<stop offset="1" stop-color="#f7b733"/>
</linearGradient>
</defs>
<rect width="128" height="128" rx="20" fill="url(#bg5)"/>
<circle cx="64" cy="50" r="26" fill="rgba(0,0,0,0.32)"/>
<path d="M22 108c10-16 26-26 42-26h0c16 0 32 10 42 26" fill="rgba(0,0,0,0.32)"/>
<circle cx="64" cy="50" r="22" fill="rgba(255,255,255,0.86)"/>
<circle cx="54" cy="50" r="5" fill="#2b0d0d"/>
<circle cx="74" cy="50" r="5" fill="#2b0d0d"/>
<rect x="50" y="62" width="28" height="6" rx="3" fill="#2b0d0d"/>
</svg>

After

Width:  |  Height:  |  Size: 694 B

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<linearGradient id="bg6" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#00f5d4"/>
<stop offset="1" stop-color="#00b4d8"/>
</linearGradient>
</defs>
<rect width="128" height="128" rx="20" fill="url(#bg6)"/>
<circle cx="64" cy="52" r="26" fill="rgba(0,0,0,0.28)"/>
<path d="M24 108c8-18 24-28 40-28h0c16 0 32 10 40 28" fill="rgba(0,0,0,0.28)"/>
<circle cx="64" cy="52" r="22" fill="rgba(255,255,255,0.88)"/>
<rect x="46" y="46" width="36" height="8" rx="4" fill="#072127"/>
<path d="M52 60h24" stroke="#072127" stroke-width="6" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 676 B

View File

@@ -416,6 +416,53 @@ button:active, .btn:active, input[type="submit"]:active{ transform: translateY(0
.actions{ display:flex; gap: 12px; margin-top: 14px; flex-wrap: wrap; }
/* Auth */
.auth-view{ display:flex; flex-direction:column; gap: 16px; }
.auth-card{ max-width: 720px; margin: 0 auto; width: 100%; }
.form-row{ margin-top: 12px; }
.input{
width: 100%;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(145,220,255,.22);
background: rgba(0,0,0,.28);
color: var(--text);
}
.input:focus{ outline: 2px solid rgba(66,245,255,.35); }
.auth-grid{
display:grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
margin-top: 12px;
}
.auth-choice{
border: 1px solid rgba(145,220,255,.18);
background: rgba(0,0,0,.18);
border-radius: 14px;
padding: 12px;
color: var(--text);
text-align: left;
display:flex;
flex-direction:column;
gap: 6px;
cursor:pointer;
}
.auth-choice:hover{ border-color: rgba(66,245,255,.45); box-shadow: var(--glow-cyan); transform: translateY(-1px); }
.auth-choice.is-selected{
border-color: rgba(66,245,255,.55);
box-shadow: var(--glow-cyan);
background: linear-gradient(180deg, rgba(66,245,255,.16), rgba(91,124,255,.10));
}
.auth-choice-title{ font-family: var(--font-sci); letter-spacing: .6px; }
.auth-choice-meta{ color: var(--muted); font-size: .85rem; }
.auth-message{ min-height: 18px; margin-top: 8px; }
.auth-avatar{
width: 100%;
border-radius: 12px;
border: 1px solid rgba(145,220,255,.18);
background: rgba(0,0,0,.22);
}
/* Resourcebar sticky */
.resourcebar{
position: sticky;

View File

@@ -119,28 +119,31 @@
});
});
const authView = document.getElementById("authView");
const gameView = document.getElementById("gameView");
let stateTimer = null;
const authPanels = {
login: document.getElementById("authLogin"),
"register-step1": document.getElementById("authRegisterStep1"),
"register-step2": document.getElementById("authRegisterStep2"),
"register-step3": document.getElementById("authRegisterStep3")
};
let racesCache = null;
let avatarsCache = null;
const regDraft = { race_key: null, avatar_key: null };
function formatNumber(val){
const num = Number.isFinite(val) ? val : 0;
return Math.round(num).toLocaleString("de-DE");
}
const resourceMap = {
"Metall": "metal",
"Kristall": "crystals",
"Deuterium": "deuterium",
"Energie": "energy"
};
function updateResourceBar(state){
const stats = document.querySelectorAll(".resource-row .stat");
stats.forEach((stat)=>{
const label = stat.querySelector(".stat-k")?.textContent?.trim();
const key = resourceMap[label];
const values = state?.resources || {};
document.querySelectorAll("[data-resource-value]").forEach((valueEl)=>{
const key = valueEl.dataset.resourceValue;
if(!key) return;
const value = state?.resources?.[key];
const value = values[key];
if(typeof value !== "number") return;
const valueEl = stat.querySelector(".stat-v");
if(!valueEl) return;
const dot = valueEl.querySelector(".dot");
const display = key === "energy" ? Math.round(value) : Math.floor(value);
const prefix = key === "energy" && display >= 0 ? "+" : "";
@@ -150,19 +153,189 @@
});
}
function updateQueuePanel(state){
const slotsEl = document.getElementById("queueSlots");
if(slotsEl){
const slots = Number.isFinite(state?.queue_slots) ? state.queue_slots : null;
slotsEl.textContent = slots === null ? "" : String(slots);
}
const list = document.getElementById("queueList");
if(!list) return;
list.textContent = "";
const jobs = Array.isArray(state?.active_build_jobs) ? state.active_build_jobs : [];
if(jobs.length === 0){
const li = document.createElement("li");
li.className = "muted";
li.textContent = "Keine aktiven Baujobs.";
list.appendChild(li);
return;
}
jobs.forEach((job)=>{
const li = document.createElement("li");
const slotIndex = Number.isFinite(Number(job?.slot_index)) ? Number(job.slot_index) + 1 : null;
const slotLabel = slotIndex ? `Slot ${slotIndex}: ` : "";
const key = job?.building_key ? String(job.building_key) : "Unbekannt";
const finish = job?.finish_at ? ` · fertig ${job.finish_at}` : "";
li.textContent = `${slotLabel}${key}${finish}`;
list.appendChild(li);
});
}
function showAuthView(){
if(authView) authView.removeAttribute("hidden");
if(gameView) gameView.setAttribute("hidden", "");
}
function showGameView(){
if(authView) authView.setAttribute("hidden", "");
if(gameView) gameView.removeAttribute("hidden");
}
function showAuthPanel(key){
Object.values(authPanels).forEach((panel)=>{
if(panel) panel.setAttribute("hidden", "");
});
const panel = authPanels[key];
if(panel) panel.removeAttribute("hidden");
}
function setMessage(el, message){
if(!el) return;
el.textContent = message || "";
}
async function fetchJson(url, options){
try{
const res = await fetch(url, options);
const data = await res.json().catch(()=> ({}));
return { ok: res.ok, status: res.status, data };
}catch(e){
return { ok: false, status: 0, data: {} };
}
}
async function loadRaces(){
if(racesCache) return racesCache;
const result = await fetchJson("/api/meta/races");
if(result.ok){
racesCache = result.data.races || [];
}else{
racesCache = [];
}
return racesCache;
}
async function loadAvatars(){
if(avatarsCache) return avatarsCache;
const result = await fetchJson("/api/meta/avatars");
if(result.ok){
avatarsCache = result.data.avatars || [];
}else{
avatarsCache = [];
}
return avatarsCache;
}
function renderRaces(races){
const list = document.getElementById("raceList");
if(!list) return;
list.textContent = "";
if(!Array.isArray(races) || races.length === 0){
const empty = document.createElement("div");
empty.className = "muted";
empty.textContent = "Keine Rassen verfügbar.";
list.appendChild(empty);
return;
}
races.forEach((race)=>{
const btn = document.createElement("button");
btn.type = "button";
btn.className = "auth-choice";
btn.dataset.raceKey = race.key;
if(regDraft.race_key === race.key) btn.classList.add("is-selected");
const summary = Array.isArray(race.modifier_summary) ? race.modifier_summary : [];
btn.innerHTML = `
<div class="auth-choice-title">${escapeHtml(race.name || race.key)}</div>
<div class="auth-choice-meta">${escapeHtml(race.description || "")}</div>
${summary.length ? `<div class="auth-choice-meta">${summary.map(s=>escapeHtml(s)).join("<br>")}</div>` : ""}
`;
btn.addEventListener("click", ()=>{
regDraft.race_key = race.key;
list.querySelectorAll(".auth-choice").forEach(el=>el.classList.remove("is-selected"));
btn.classList.add("is-selected");
});
list.appendChild(btn);
});
}
function renderAvatars(avatars){
const list = document.getElementById("avatarList");
if(!list) return;
list.textContent = "";
if(!Array.isArray(avatars) || avatars.length === 0){
const empty = document.createElement("div");
empty.className = "muted";
empty.textContent = "Keine Avatare verfügbar.";
list.appendChild(empty);
return;
}
avatars.forEach((avatar)=>{
const btn = document.createElement("button");
btn.type = "button";
btn.className = "auth-choice";
btn.dataset.avatarKey = avatar.key;
if(regDraft.avatar_key === avatar.key) btn.classList.add("is-selected");
btn.innerHTML = `
<img class="auth-avatar" src="${escapeHtml(avatar.image || "")}" alt="${escapeHtml(avatar.label || avatar.key)}">
<div class="auth-choice-title">${escapeHtml(avatar.label || avatar.key)}</div>
`;
btn.addEventListener("click", ()=>{
regDraft.avatar_key = avatar.key;
list.querySelectorAll(".auth-choice").forEach(el=>el.classList.remove("is-selected"));
btn.classList.add("is-selected");
});
list.appendChild(btn);
});
}
function startPolling(){
if(stateTimer) return;
stateTimer = setInterval(refreshState, 15000);
}
function stopPolling(){
if(!stateTimer) return;
clearInterval(stateTimer);
stateTimer = null;
}
async function fetchState(){
try{
const res = await fetch("/api/state");
if(!res.ok) return null;
return await res.json();
const res = await fetch("/api/state", { credentials: "same-origin" });
if(res.status === 401) return { status: 401 };
if(!res.ok) return { status: res.status };
const data = await res.json();
return { status: 200, data };
}catch(e){
return null;
return { status: 0 };
}
}
async function refreshState(){
const state = await fetchState();
if(state) updateResourceBar(state);
const result = await fetchState();
if(result.status === 200){
showGameView();
updateResourceBar(result.data);
updateQueuePanel(result.data);
ensureBuildButton();
startPolling();
return;
}
if(result.status === 401){
showAuthView();
showAuthPanel("login");
stopPolling();
}
}
function ensureBuildButton(){
@@ -198,8 +371,130 @@
}
document.addEventListener("DOMContentLoaded", ()=>{
document.querySelectorAll("[data-auth-switch]").forEach((btn)=>{
btn.addEventListener("click", async ()=>{
const target = btn.dataset.authSwitch;
if(!target) return;
showAuthView();
showAuthPanel(target);
if(target === "register-step1"){
const races = await loadRaces();
renderRaces(races);
}
if(target === "register-step2"){
const avatars = await loadAvatars();
renderAvatars(avatars);
}
});
});
const loginForm = document.getElementById("loginForm");
if(loginForm){
loginForm.addEventListener("submit", async (e)=>{
e.preventDefault();
const identifier = document.getElementById("loginIdentifier")?.value?.trim() || "";
const password = document.getElementById("loginPassword")?.value || "";
const message = document.getElementById("loginMessage");
setMessage(message, "");
const result = await fetchJson("/api/auth/login", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ username_or_email: identifier, password })
});
if(result.ok){
showGameView();
refreshState();
}else{
setMessage(message, result.data?.message || "Login fehlgeschlagen.");
}
});
}
const raceNext = document.getElementById("raceNext");
if(raceNext){
raceNext.addEventListener("click", async ()=>{
const message = document.getElementById("raceMessage");
setMessage(message, "");
if(!regDraft.race_key){
setMessage(message, "Bitte eine Rasse wählen.");
return;
}
const result = await fetchJson("/api/auth/register/step1", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ race_key: regDraft.race_key })
});
if(result.ok){
showAuthPanel("register-step2");
const avatars = await loadAvatars();
renderAvatars(avatars);
}else{
setMessage(message, result.data?.message || "Schritt 1 fehlgeschlagen.");
}
});
}
const avatarNext = document.getElementById("avatarNext");
if(avatarNext){
avatarNext.addEventListener("click", async ()=>{
const title = document.getElementById("regTitle")?.value?.trim() || "";
const message = document.getElementById("avatarMessage");
setMessage(message, "");
if(!regDraft.avatar_key){
setMessage(message, "Bitte einen Avatar wählen.");
return;
}
if(title.length < 2){
setMessage(message, "Titel ist zu kurz.");
return;
}
const result = await fetchJson("/api/auth/register/step2", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ avatar_key: regDraft.avatar_key, title })
});
if(result.ok){
showAuthPanel("register-step3");
}else{
setMessage(message, result.data?.message || "Schritt 2 fehlgeschlagen.");
}
});
}
const registerForm = document.getElementById("registerForm");
if(registerForm){
registerForm.addEventListener("submit", async (e)=>{
e.preventDefault();
const username = document.getElementById("regUsername")?.value?.trim() || "";
const email = document.getElementById("regEmail")?.value?.trim() || "";
const password = document.getElementById("regPassword")?.value || "";
const message = document.getElementById("registerMessage");
setMessage(message, "");
const result = await fetchJson("/api/auth/register/step3", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ username, email, password })
});
if(result.ok){
showGameView();
refreshState();
}else{
setMessage(message, result.data?.message || "Registrierung fehlgeschlagen.");
}
});
}
const logoutBtn = document.getElementById("logoutBtn");
if(logoutBtn){
logoutBtn.addEventListener("click", async ()=>{
await fetchJson("/api/auth/logout", { method: "POST" });
stopPolling();
showAuthView();
showAuthPanel("login");
});
}
showAuthPanel("login");
refreshState();
ensureBuildButton();
setInterval(refreshState, 30000);
});
})();

View File

@@ -103,7 +103,14 @@ $partialsPath = __DIR__ . '/../src/partials';
<div class="space-bg" aria-hidden="true"></div>
<div class="container">
<div class="app">
<div class="auth-view" id="authView" hidden>
<?php include $partialsPath . '/auth-login.php'; ?>
<?php include $partialsPath . '/auth-register-step1.php'; ?>
<?php include $partialsPath . '/auth-register-step2.php'; ?>
<?php include $partialsPath . '/auth-register-step3.php'; ?>
</div>
<div class="app" id="gameView">
<!-- LINKS: Sidebar -->
<aside class="sidebar" aria-label="Seitenleiste">
@@ -132,6 +139,7 @@ $partialsPath = __DIR__ . '/../src/partials';
🔔 <span class="badge" id="notifBadge">3</span>
</button>
<button class="btn btn-primary" type="button" onclick="toast('success','Beacon','Signal empfangen ✅')">Test Toast</button>
<button class="btn" type="button" id="logoutBtn">Logout</button>
</div>
</div>
@@ -211,6 +219,14 @@ $partialsPath = __DIR__ . '/../src/partials';
</div>
<?php include $partialsPath . '/site.php'; ?>
<section class="card inner panel" id="queuePanel" style="margin-top:14px;">
<h2 class="h2">Bauqueue (Live)</h2>
<div class="muted">Slots: <span id="queueSlots">0</span></div>
<ul id="queueList">
<li class="muted">Keine aktiven Baujobs.</li>
</ul>
</section>
</main>
<!-- Footer -->

View File

@@ -0,0 +1,23 @@
<section class="card panel auth-card" id="authLogin">
<div class="panel-title">LOGIN</div>
<p class="muted">Melde dich an, um deine Kolonie zu laden.</p>
<form id="loginForm">
<div class="form-row">
<label class="label" for="loginIdentifier">Username oder E-Mail</label>
<input class="input" id="loginIdentifier" name="username_or_email" type="text" autocomplete="username" required>
</div>
<div class="form-row">
<label class="label" for="loginPassword">Passwort</label>
<input class="input" id="loginPassword" name="password" type="password" autocomplete="current-password" required>
</div>
<div class="actions">
<button class="btn btn-primary" type="submit">Login</button>
<button class="btn" type="button" data-auth-switch="register-step1">Registrieren</button>
</div>
<div class="auth-message muted tiny" id="loginMessage" aria-live="polite"></div>
</form>
</section>

View File

@@ -0,0 +1,16 @@
<section class="card panel auth-card" id="authRegisterStep1" hidden>
<div class="panel-title">REGISTRIERUNG 1/3</div>
<h2 class="h2">Wähle deine Rasse</h2>
<p class="muted">Rasse bestimmt Startboni. Du kannst später über Forschungen nachsteuern.</p>
<div class="auth-grid" id="raceList">
<div class="muted">Lade Rassen ...</div>
</div>
<div class="actions">
<button class="btn" type="button" data-auth-switch="login">Zurück</button>
<button class="btn btn-primary" type="button" id="raceNext">Weiter</button>
</div>
<div class="auth-message muted tiny" id="raceMessage" aria-live="polite"></div>
</section>

View File

@@ -0,0 +1,22 @@
<section class="card panel auth-card" id="authRegisterStep2" hidden>
<div class="panel-title">REGISTRIERUNG 2/3</div>
<h2 class="h2">Avatar & Titel</h2>
<p class="muted">Wähle einen Avatar und gib deinem Captain einen Titel.</p>
<div class="auth-grid avatar-grid" id="avatarList">
<div class="muted">Lade Avatare ...</div>
</div>
<div class="form-row">
<label class="label" for="regTitle">Titel</label>
<input class="input" id="regTitle" name="title" type="text" maxlength="40" placeholder="z.B. Pionier, Architekt, Navigator" required>
<div class="muted tiny">240 Zeichen.</div>
</div>
<div class="actions">
<button class="btn" type="button" data-auth-switch="register-step1">Zurück</button>
<button class="btn btn-primary" type="button" id="avatarNext">Weiter</button>
</div>
<div class="auth-message muted tiny" id="avatarMessage" aria-live="polite"></div>
</section>

View File

@@ -0,0 +1,30 @@
<section class="card panel auth-card" id="authRegisterStep3" hidden>
<div class="panel-title">REGISTRIERUNG 3/3</div>
<h2 class="h2">Account erstellen</h2>
<p class="muted">Wähle deinen Accountnamen und sichere dein Profil.</p>
<form id="registerForm">
<div class="form-row">
<label class="label" for="regUsername">Username</label>
<input class="input" id="regUsername" name="username" type="text" autocomplete="username" required>
</div>
<div class="form-row">
<label class="label" for="regEmail">E-Mail</label>
<input class="input" id="regEmail" name="email" type="email" autocomplete="email" required>
</div>
<div class="form-row">
<label class="label" for="regPassword">Passwort</label>
<input class="input" id="regPassword" name="password" type="password" autocomplete="new-password" required>
<div class="muted tiny">Mindestens 8 Zeichen.</div>
</div>
<div class="actions">
<button class="btn" type="button" data-auth-switch="register-step2">Zurück</button>
<button class="btn btn-primary" type="submit">Account erstellen</button>
</div>
<div class="auth-message muted tiny" id="registerMessage" aria-live="polite"></div>
</form>
</section>

View File

@@ -5,24 +5,49 @@
</div>
<div class="stats">
<div class="stat">
<div class="stat" data-resource="metal">
<div class="stat-k">Metall</div>
<div class="stat-v"><span class="dot dot-cyan"></span> 12.340</div>
<div class="stat-v" id="res-metal" data-resource-value="metal"><span class="dot dot-cyan"></span> 12.340</div>
<div class="stat-bar"><span style="width:72%"></span></div>
</div>
<div class="stat">
<div class="stat" data-resource="crystals">
<div class="stat-k">Kristall</div>
<div class="stat-v"><span class="dot dot-pink"></span> 6.120</div>
<div class="stat-v" id="res-crystals" data-resource-value="crystals"><span class="dot dot-pink"></span> 6.120</div>
<div class="stat-bar"><span style="width:44%"></span></div>
</div>
<div class="stat">
<div class="stat" data-resource="deuterium">
<div class="stat-k">Deuterium</div>
<div class="stat-v"><span class="dot dot-green"></span> 3.880</div>
<div class="stat-v" id="res-deuterium" data-resource-value="deuterium"><span class="dot dot-green"></span> 3.880</div>
<div class="stat-bar"><span style="width:18%"></span></div>
</div>
<div class="stat">
<div class="stat" data-resource="energy">
<div class="stat-k">Energie</div>
<div class="stat-v"><span class="dot dot-warn"></span> +120</div>
<div class="stat-v" id="res-energy" data-resource-value="energy"><span class="dot dot-warn"></span> +120</div>
<div class="stat-bar"><span style="width:60%"></span></div>
</div>
<div class="stat" data-resource="alloy">
<div class="stat-k">Legierung</div>
<div class="stat-v" id="res-alloy" data-resource-value="alloy"><span class="dot dot-cyan"></span> 0</div>
<div class="stat-bar"><span style="width:40%"></span></div>
</div>
<div class="stat" data-resource="credits">
<div class="stat-k">Credits</div>
<div class="stat-v" id="res-credits" data-resource-value="credits"><span class="dot dot-pink"></span> 0</div>
<div class="stat-bar"><span style="width:35%"></span></div>
</div>
<div class="stat" data-resource="population">
<div class="stat-k">Bevölkerung</div>
<div class="stat-v" id="res-population" data-resource-value="population"><span class="dot dot-green"></span> 0</div>
<div class="stat-bar"><span style="width:55%"></span></div>
</div>
<div class="stat" data-resource="water">
<div class="stat-k">Wasser</div>
<div class="stat-v" id="res-water" data-resource-value="water"><span class="dot dot-cyan"></span> 0</div>
<div class="stat-bar"><span style="width:30%"></span></div>
</div>
<div class="stat" data-resource="food">
<div class="stat-k">Nahrung</div>
<div class="stat-v" id="res-food" data-resource-value="food"><span class="dot dot-green"></span> 0</div>
<div class="stat-bar"><span style="width:60%"></span></div>
</div>
</div>