This commit is contained in:
cay 2026-04-14 18:42:30 +01:00
parent b486199ca2
commit 109842b8de
6 changed files with 1505 additions and 6 deletions

2
app.js
View File

@ -33,6 +33,7 @@ const combineRoutes = require("./routes/combine.route");
const bazaarRoutes = require("./routes/bazaar.route");
const himmelstorRoutes = require("./routes/himmelstor.route");
const himmelstorDailyRoutes = require("./routes/himmelstor-daily.route");
const gildenhalleRoutes = require("./routes/gildenhalle.route");
const compression = require("compression");
@ -433,6 +434,7 @@ app.use("/api", combineRoutes);
app.use("/api", bazaarRoutes);
app.use("/himmelstor", himmelstorRoutes);
app.use("/api/himmelstor/daily", himmelstorDailyRoutes);
app.use("/api", gildenhalleRoutes);
/* ========================
Energie abfragen

522
public/css/gildenhalle.css Normal file
View File

@ -0,0 +1,522 @@
/* ============================================================
public/css/gildenhalle.css
============================================================ */
/* ── Gilden-spezifische Farben ───────────────────────────── */
:root {
--guild-gold: #f0d060;
--guild-border: #8b6a3c;
--guild-dark: #1a0f04;
--guild-mid: #2a1a08;
--guild-text: #f0d9a6;
--guild-muted: #a08060;
--guild-green: #4aaa30;
--guild-red: #cc3030;
}
/* ── Mitglieder-Tabelle ─────────────────────────────────── */
.gh-members-table {
width: 100%;
border-collapse: collapse;
font-family: "Cinzel", serif;
font-size: 12px;
}
.gh-members-table th {
color: var(--guild-muted);
font-size: 10px;
letter-spacing: 1px;
text-transform: uppercase;
padding: 6px 10px;
border-bottom: 1px solid var(--guild-border);
text-align: left;
}
.gh-members-table td {
padding: 7px 10px;
border-bottom: 1px solid rgba(139,106,42,.2);
color: var(--guild-text);
vertical-align: middle;
}
.gh-members-table tr:hover td {
background: rgba(255,255,255,.03);
}
/* ── Rang-Badge ─────────────────────────────────────────── */
.gh-rank-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: bold;
background: linear-gradient(#3a2810, #1a0f04);
border: 1px solid var(--guild-border);
color: var(--guild-gold);
white-space: nowrap;
}
.gh-rank-badge.leader {
border-color: var(--guild-gold);
background: linear-gradient(#6b4b2a, #3c2414);
box-shadow: 0 0 6px rgba(200,160,60,.3);
}
/* ── Gilden-Karten im Suchraster ────────────────────────── */
.gh-guild-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
padding: 12px 14px;
overflow-y: auto;
align-content: start;
}
.gh-guild-card {
background: linear-gradient(135deg, var(--guild-mid), var(--guild-dark));
border: 2px solid var(--guild-border);
border-radius: 10px;
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 6px;
transition: .2s;
cursor: default;
}
.gh-guild-card:hover {
border-color: var(--guild-gold);
box-shadow: 0 4px 16px rgba(0,0,0,.5), 0 0 10px rgba(200,160,60,.15);
}
.gh-guild-header {
display: flex;
align-items: center;
gap: 8px;
}
.gh-guild-tag {
background: linear-gradient(#6b4b2a, #3c2414);
border: 1px solid var(--guild-gold);
border-radius: 6px;
padding: 2px 8px;
font-family: "Cinzel", serif;
font-size: 12px;
font-weight: bold;
color: var(--guild-gold);
flex-shrink: 0;
}
.gh-guild-name {
font-family: "Cinzel", serif;
font-size: 13px;
font-weight: bold;
color: var(--guild-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gh-guild-desc {
font-family: "Cinzel", serif;
font-size: 10px;
color: var(--guild-muted);
line-height: 1.5;
flex: 1;
}
.gh-guild-meta {
display: flex;
justify-content: space-between;
font-family: "Cinzel", serif;
font-size: 10px;
color: var(--guild-muted);
}
.gh-guild-open {
color: var(--guild-green);
font-weight: bold;
}
.gh-guild-closed {
color: #e0a030;
font-weight: bold;
}
.gh-join-btn {
margin-top: 4px;
padding: 5px 0;
background: linear-gradient(#3a2810, #1a0f04);
border: 2px solid var(--guild-border);
border-radius: 6px;
color: var(--guild-text);
font-family: "Cinzel", serif;
font-size: 11px;
font-weight: bold;
cursor: pointer;
transition: .15s;
width: 100%;
}
.gh-join-btn:hover:not(:disabled) {
border-color: var(--guild-gold);
color: #fff;
}
.gh-join-btn:disabled {
opacity: .45;
cursor: not-allowed;
}
.gh-join-btn.pending {
border-color: #e0a030;
color: #e0a030;
}
/* ── Gilde gründen ──────────────────────────────────────── */
.gh-create-form {
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 12px;
max-width: 440px;
}
.gh-form-row {
display: flex;
flex-direction: column;
gap: 4px;
}
.gh-form-label {
font-family: "Cinzel", serif;
font-size: 11px;
color: var(--guild-muted);
letter-spacing: 1px;
}
.gh-form-input,
.gh-form-textarea,
.gh-form-select {
background: rgba(0,0,0,.5);
border: 2px solid var(--guild-border);
border-radius: 7px;
color: var(--guild-text);
font-family: "Cinzel", serif;
font-size: 13px;
padding: 7px 10px;
outline: none;
transition: border-color .15s;
}
.gh-form-input:focus,
.gh-form-textarea:focus,
.gh-form-select:focus { border-color: var(--guild-gold); }
.gh-form-textarea { resize: vertical; min-height: 70px; }
.gh-form-select { cursor: pointer; }
.gh-create-btn {
padding: 10px 0;
background: linear-gradient(#6b4b2a, #3c2414);
border: 2px solid var(--guild-gold);
border-radius: 8px;
color: #fff5cc;
font-family: "Cinzel", serif;
font-size: 13px;
font-weight: bold;
cursor: pointer;
transition: .2s;
letter-spacing: 2px;
}
.gh-create-btn:hover {
box-shadow: 0 0 14px rgba(200,160,60,.4);
}
.gh-error-msg {
font-family: "Cinzel", serif;
font-size: 11px;
color: #ff6666;
text-align: center;
}
/* ── Aufgaben ────────────────────────────────────────────── */
.gh-tasks-list {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px 16px;
overflow-y: auto;
}
.gh-task-card {
background: linear-gradient(135deg, var(--guild-mid), var(--guild-dark));
border: 2px solid var(--guild-border);
border-radius: 10px;
padding: 12px 16px;
display: flex;
flex-direction: column;
gap: 8px;
transition: .2s;
}
.gh-task-card.completed {
border-color: var(--guild-green);
opacity: .75;
}
.gh-task-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.gh-task-label {
font-family: "Cinzel", serif;
font-size: 13px;
color: var(--guild-text);
font-weight: bold;
}
.gh-task-reward {
font-family: "Cinzel", serif;
font-size: 11px;
color: var(--guild-gold);
}
.gh-task-done-badge {
font-family: "Cinzel", serif;
font-size: 11px;
color: var(--guild-green);
font-weight: bold;
}
.gh-task-progress-wrap {
display: flex;
flex-direction: column;
gap: 4px;
}
.gh-task-bar-track {
height: 8px;
background: rgba(255,255,255,.08);
border-radius: 4px;
overflow: hidden;
border: 1px solid rgba(139,106,42,.3);
}
.gh-task-bar-fill {
height: 100%;
background: linear-gradient(90deg, #4a8a3c, #7ada60);
border-radius: 4px;
transition: width .5s ease;
}
.gh-task-bar-fill.completed {
background: linear-gradient(90deg, var(--guild-green), #8aff60);
}
.gh-task-progress-text {
font-family: "Cinzel", serif;
font-size: 10px;
color: var(--guild-muted);
display: flex;
justify-content: space-between;
}
/* ── Anfragen-Liste ─────────────────────────────────────── */
.gh-requests-list {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px 0;
}
.gh-request-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: rgba(0,0,0,.25);
border: 1px solid rgba(139,106,42,.25);
border-radius: 7px;
gap: 10px;
}
.gh-request-name {
font-family: "Cinzel", serif;
font-size: 12px;
color: var(--guild-text);
flex: 1;
}
.gh-request-msg {
font-family: "Cinzel", serif;
font-size: 10px;
color: var(--guild-muted);
font-style: italic;
flex: 2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gh-req-accept,
.gh-req-reject {
padding: 4px 12px;
border-radius: 5px;
font-family: "Cinzel", serif;
font-size: 10px;
font-weight: bold;
cursor: pointer;
transition: .15s;
flex-shrink: 0;
}
.gh-req-accept {
background: linear-gradient(#1a4a18, #0f2a0e);
border: 2px solid #4a8a3c;
color: #a0e090;
}
.gh-req-accept:hover { border-color: #7ada60; }
.gh-req-reject {
background: linear-gradient(#4a1010, #2a0808);
border: 2px solid #8a3030;
color: #e07070;
}
.gh-req-reject:hover { border-color: #ff6060; }
/* ── Meine Gilde Info-Box ───────────────────────────────── */
.gh-my-info {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: rgba(0,0,0,.3);
border-bottom: 1px solid var(--guild-border);
flex-shrink: 0;
}
.gh-my-tag {
font-family: "Cinzel", serif;
font-size: 20px;
font-weight: bold;
color: var(--guild-gold);
background: linear-gradient(#6b4b2a, #3c2414);
border: 2px solid var(--guild-gold);
border-radius: 8px;
padding: 4px 12px;
}
.gh-my-name {
font-family: "Cinzel", serif;
font-size: 16px;
font-weight: bold;
color: var(--guild-text);
}
.gh-my-sub {
font-family: "Cinzel", serif;
font-size: 11px;
color: var(--guild-muted);
}
.gh-leave-btn {
margin-left: auto;
padding: 6px 16px;
background: linear-gradient(#3a1010, #1a0808);
border: 2px solid #6a2020;
border-radius: 7px;
color: #d08080;
font-family: "Cinzel", serif;
font-size: 11px;
cursor: pointer;
transition: .15s;
}
.gh-leave-btn:hover { border-color: #cc4040; color: #ff9090; }
/* ── Suche ──────────────────────────────────────────────── */
.gh-search-bar {
display: flex;
gap: 8px;
padding: 10px 14px;
border-bottom: 1px solid rgba(139,106,42,.3);
flex-shrink: 0;
}
.gh-search-input {
flex: 1;
background: rgba(0,0,0,.5);
border: 2px solid var(--guild-border);
border-radius: 7px;
color: var(--guild-text);
font-family: "Cinzel", serif;
font-size: 12px;
padding: 6px 10px;
outline: none;
}
.gh-search-input:focus { border-color: var(--guild-gold); }
.gh-search-btn {
padding: 6px 16px;
background: linear-gradient(#3a2810, #1a0f04);
border: 2px solid var(--guild-border);
border-radius: 7px;
color: var(--guild-text);
font-family: "Cinzel", serif;
font-size: 12px;
cursor: pointer;
transition: .15s;
}
.gh-search-btn:hover { border-color: var(--guild-gold); }
/* ── Keine Gilde Hinweis ────────────────────────────────── */
.gh-no-guild {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 12px;
font-family: "Cinzel", serif;
color: var(--guild-muted);
text-align: center;
padding: 24px;
}
.gh-no-guild-icon { font-size: 42px; }
.gh-no-guild-text { font-size: 13px; line-height: 1.8; }
/* ── Pagination ─────────────────────────────────────────── */
.gh-pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
padding: 8px;
border-top: 1px solid rgba(139,106,42,.3);
flex-shrink: 0;
}
.gh-page-btn {
background: linear-gradient(#3a2810, #1a0f04);
border: 1px solid var(--guild-border);
border-radius: 5px;
color: var(--guild-text);
font-family: "Cinzel", serif;
font-size: 11px;
padding: 3px 10px;
cursor: pointer;
transition: .15s;
}
.gh-page-btn:hover:not(:disabled) { border-color: var(--guild-gold); }
.gh-page-btn:disabled { opacity: .35; cursor: not-allowed; }
.gh-page-btn.active {
background: linear-gradient(#6b4b2a, #3c2414);
border-color: var(--guild-gold);
}
/* ── Lade/Leer-Zustand ──────────────────────────────────── */
.gh-loading, .gh-empty {
text-align: center;
padding: 40px;
font-family: "Cinzel", serif;
font-size: 13px;
color: var(--guild-muted);
}
/* ── Section-Header innerhalb Panel ────────────────────── */
.gh-section-title {
font-family: "Cinzel", serif;
font-size: 12px;
color: var(--guild-gold);
letter-spacing: 2px;
text-transform: uppercase;
padding: 8px 16px 4px;
border-bottom: 1px solid rgba(139,106,42,.25);
flex-shrink: 0;
}
/* ── Rang-Select in Mitglieder-Tabelle ──────────────────── */
.gh-rank-select {
background: rgba(0,0,0,.5);
border: 1px solid var(--guild-border);
border-radius: 5px;
color: var(--guild-text);
font-family: "Cinzel", serif;
font-size: 11px;
padding: 2px 5px;
cursor: pointer;
}
.gh-kick-btn {
padding: 2px 8px;
background: linear-gradient(#4a1010, #2a0808);
border: 1px solid #8a3030;
border-radius: 5px;
color: #e07070;
font-family: "Cinzel", serif;
font-size: 10px;
cursor: pointer;
transition: .15s;
}
.gh-kick-btn:hover { border-color: #cc4040; }

View File

@ -0,0 +1,547 @@
/* ============================================================
public/js/buildings/gildenhalle.js
============================================================ */
let gh_initialized = false;
let gh_page = 1;
let gh_search = '';
let gh_playerGuild = null;
function ghLoadCSS() {
if (!document.querySelector('link[href="/css/gildenhalle.css"]')) {
const l = document.createElement('link');
l.rel = 'stylesheet'; l.href = '/css/gildenhalle.css';
document.head.appendChild(l);
}
}
/* ── Popup sicherstellen ────────────────────────────────── */
function ghEnsurePopup() {
if (document.getElementById('gildenhalle-popup')) return;
const popup = document.createElement('div');
popup.id = 'gildenhalle-popup';
popup.className = 'qm-popup';
popup.innerHTML = `
<div class="qm-popup-header">
<span class="qm-popup-title"> Gildenhalle</span>
<div class="baz-header-right">
<span class="qm-popup-close" id="gh-close"></span>
</div>
</div>
<div class="mp-body-wrap">
<aside class="mp-tabs" id="gh-tabs">
<button class="mp-tab mp-tab-active" data-tab="gh-panel-1">
<span class="mp-tab-dot"></span><span>Gilden</span>
</button>
<button class="mp-tab" data-tab="gh-panel-2">
<span class="mp-tab-dot"></span><span>Eigene Gilde</span>
</button>
<button class="mp-tab" data-tab="gh-panel-3">
<span class="mp-tab-dot"></span><span>Gilden Aufgaben</span>
</button>
</aside>
<div class="mp-content">
<!-- PANEL 1: Gilden suchen -->
<div class="mp-panel active" id="gh-panel-1" style="overflow:hidden;">
<div class="mp-col-header">Gilden</div>
<div class="gh-search-bar">
<input class="gh-search-input" id="gh-search-input"
placeholder="Gildennamen oder Tag suchen…" />
<button class="gh-search-btn" id="gh-search-btn">Suchen</button>
<button class="gh-search-btn" id="gh-create-toggle-btn"
style="border-color:#f0d060;color:#f0d060;">+ Gründen</button>
</div>
<div class="gh-guild-grid" id="gh-guild-grid">
<div class="gh-loading">Lade Gilden</div>
</div>
<div class="gh-pagination" id="gh-pagination"></div>
<!-- Gründen-Formular (versteckt) -->
<div id="gh-create-form-wrap" style="display:none;position:absolute;inset:0;
background:rgba(10,6,2,.96);z-index:20;overflow-y:auto;">
<div class="gh-create-form">
<div class="mp-col-header" style="margin-bottom:4px;">Neue Gilde gründen</div>
<div class="gh-form-row">
<label class="gh-form-label">Gildenname *</label>
<input class="gh-form-input" id="gh-new-name" maxlength="50" placeholder="Name der Gilde">
</div>
<div class="gh-form-row">
<label class="gh-form-label">Tag (26 Zeichen) *</label>
<input class="gh-form-input" id="gh-new-tag" maxlength="6" placeholder="z.B. DOK"
style="text-transform:uppercase;">
</div>
<div class="gh-form-row">
<label class="gh-form-label">Beschreibung</label>
<textarea class="gh-form-textarea" id="gh-new-desc" maxlength="255"
placeholder="Kurze Beschreibung…"></textarea>
</div>
<div class="gh-form-row">
<label class="gh-form-label">Beitrittsmodus</label>
<select class="gh-form-select" id="gh-new-open">
<option value="1">Offen jeder kann beitreten</option>
<option value="0">Geschlossen nur per Anfrage</option>
</select>
</div>
<div class="gh-error-msg" id="gh-create-error"></div>
<button class="gh-create-btn" id="gh-create-submit"> GILDE GRÜNDEN</button>
<button class="gh-search-btn" id="gh-create-cancel"
style="text-align:center;">Abbrechen</button>
</div>
</div>
</div>
<!-- PANEL 2: Eigene Gilde -->
<div class="mp-panel" id="gh-panel-2" style="overflow:hidden;flex-direction:column;">
<div class="mp-col-header">Eigene Gilde</div>
<div id="gh-my-content" style="flex:1;overflow-y:auto;display:flex;flex-direction:column;">
<div class="gh-loading">Lade</div>
</div>
</div>
<!-- PANEL 3: Gilden Aufgaben -->
<div class="mp-panel" id="gh-panel-3" style="overflow:hidden;flex-direction:column;">
<div class="mp-col-header">Gilden Aufgaben</div>
<div id="gh-tasks-content" style="flex:1;overflow-y:auto;">
<div class="gh-loading">Lade Aufgaben</div>
</div>
</div>
</div>
</div>`;
document.body.appendChild(popup);
popup.querySelector('#gh-close').addEventListener('click', ghClose);
/* Tab-Wechsel */
popup.querySelectorAll('.mp-tab').forEach(btn => {
btn.addEventListener('click', () => {
popup.querySelectorAll('.mp-tab').forEach(t => t.classList.remove('mp-tab-active'));
popup.querySelectorAll('.mp-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('mp-tab-active');
const panel = document.getElementById(btn.dataset.tab);
panel?.classList.add('active');
if (btn.dataset.tab === 'gh-panel-2') ghLoadMyGuild();
if (btn.dataset.tab === 'gh-panel-3') ghLoadTasks();
});
});
/* Gründen-Toggle */
document.getElementById('gh-create-toggle-btn').addEventListener('click', () => {
document.getElementById('gh-create-form-wrap').style.display = 'block';
document.getElementById('gh-create-error').textContent = '';
});
document.getElementById('gh-create-cancel').addEventListener('click', () => {
document.getElementById('gh-create-form-wrap').style.display = 'none';
});
document.getElementById('gh-create-submit').addEventListener('click', ghCreateGuild);
/* Suche */
document.getElementById('gh-search-btn').addEventListener('click', () => {
gh_search = document.getElementById('gh-search-input').value.trim();
gh_page = 1;
ghLoadGuildList();
});
document.getElementById('gh-search-input').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('gh-search-btn').click();
});
/* Drag */
const header = popup.querySelector('.qm-popup-header');
let dragging=false, sx,sy,sl,st;
header.style.cursor='grab';
header.addEventListener('mousedown', e => {
if (e.target.classList.contains('qm-popup-close')) return;
dragging=true; header.style.cursor='grabbing';
const r=popup.getBoundingClientRect();
sx=e.clientX; sy=e.clientY; sl=r.left; st=r.top;
popup.style.transform='none';
popup.style.left=sl+'px'; popup.style.top=st+'px';
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (!dragging) return;
popup.style.left=(sl+(e.clientX-sx))+'px';
popup.style.top=(st+(e.clientY-sy))+'px';
});
document.addEventListener('mouseup', () => { dragging=false; header.style.cursor='grab'; });
}
/*
PANEL 1: Gilden-Liste laden
*/
async function ghLoadGuildList() {
const grid = document.getElementById('gh-guild-grid');
const pagination = document.getElementById('gh-pagination');
if (!grid) return;
grid.innerHTML = '<div class="gh-loading">Lade Gilden…</div>';
if (pagination) pagination.innerHTML = '';
try {
const res = await fetch(`/api/gildenhalle/list?search=${encodeURIComponent(gh_search)}&page=${gh_page}`);
const data = await res.json();
gh_playerGuild = data.playerGuildId;
if (!data.guilds.length) {
grid.innerHTML = '<div class="gh-empty">Keine Gilden gefunden.</div>';
return;
}
grid.innerHTML = data.guilds.map(g => `
<div class="gh-guild-card">
<div class="gh-guild-header">
<span class="gh-guild-tag">[${g.tag}]</span>
<span class="gh-guild-name">${g.name}</span>
</div>
<div class="gh-guild-desc">${g.description || '<em>Keine Beschreibung</em>'}</div>
<div class="gh-guild-meta">
<span>Lv. ${g.level} · ${g.member_count}/${g.max_members} Mitgl.</span>
<span class="${g.open ? 'gh-guild-open' : 'gh-guild-closed'}">
${g.open ? '✔ Offen' : '🔒 Anfrage'}
</span>
</div>
<button class="gh-join-btn ${g.has_pending_request ? 'pending' : ''}"
data-id="${g.id}" data-open="${g.open}"
${gh_playerGuild || g.has_pending_request ? 'disabled' : ''}>
${g.has_pending_request ? '⏳ Anfrage ausstehend'
: gh_playerGuild === g.id ? '✔ Meine Gilde'
: gh_playerGuild ? '(In Gilde)'
: g.open ? '⚔ Beitreten' : '✉ Anfrage stellen'}
</button>
</div>`).join('');
grid.querySelectorAll('.gh-join-btn:not([disabled])').forEach(btn => {
btn.addEventListener('click', () => ghJoinGuild(
parseInt(btn.dataset.id), btn.dataset.open === '1', btn
));
});
ghRenderPagination(pagination, data.totalPages, data.total);
} catch (err) {
grid.innerHTML = '<div class="gh-empty">Fehler beim Laden.</div>';
console.error('[GH]', err);
}
}
/* ── Gilde beitreten ────────────────────────────────────── */
async function ghJoinGuild(guildId, isOpen, btn) {
btn.disabled = true;
btn.textContent = '…';
let message = null;
if (!isOpen) {
message = window.prompt('Nachricht an die Gilde (optional):') ?? '';
}
try {
const res = await fetch(`/api/gildenhalle/join/${guildId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
});
const data = await res.json();
if (!res.ok) { alert(data.error || 'Fehler'); btn.disabled=false; btn.textContent=isOpen?'⚔ Beitreten':'✉ Anfrage stellen'; return; }
if (data.joined) {
ghShowToast('✔ Erfolgreich der Gilde beigetreten!');
gh_page = 1; ghLoadGuildList();
} else {
btn.classList.add('pending');
btn.textContent = '⏳ Anfrage ausstehend';
ghShowToast('✉ Beitrittsanfrage gesendet!');
}
} catch { btn.disabled=false; }
}
/* ── Gilde gründen ──────────────────────────────────────── */
async function ghCreateGuild() {
const name = document.getElementById('gh-new-name').value.trim();
const tag = document.getElementById('gh-new-tag').value.trim().toUpperCase();
const desc = document.getElementById('gh-new-desc').value.trim();
const open = document.getElementById('gh-new-open').value;
const errEl = document.getElementById('gh-create-error');
errEl.textContent = '';
if (!name || !tag) { errEl.textContent = 'Name und Tag sind Pflichtfelder.'; return; }
const btn = document.getElementById('gh-create-submit');
btn.disabled = true; btn.textContent = '…';
try {
const res = await fetch('/api/gildenhalle/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, tag, description: desc, open: open === '1' }),
});
const data = await res.json();
if (!res.ok) { errEl.textContent = data.error || 'Fehler'; btn.disabled=false; btn.textContent='⚔ GILDE GRÜNDEN'; return; }
document.getElementById('gh-create-form-wrap').style.display = 'none';
ghShowToast('🏰 Gilde erfolgreich gegründet!');
gh_page = 1; ghLoadGuildList();
// Wechsel zu "Eigene Gilde"
setTimeout(() => {
document.querySelector('[data-tab="gh-panel-2"]')?.click();
}, 500);
} catch { btn.disabled=false; btn.textContent='⚔ GILDE GRÜNDEN'; }
}
/*
PANEL 2: Eigene Gilde
*/
async function ghLoadMyGuild() {
const container = document.getElementById('gh-my-content');
if (!container) return;
container.innerHTML = '<div class="gh-loading">Lade…</div>';
try {
const res = await fetch('/api/gildenhalle/my');
const data = await res.json();
if (!data.guild) {
container.innerHTML = `
<div class="gh-no-guild">
<div class="gh-no-guild-icon">🏰</div>
<div class="gh-no-guild-text">Du bist noch in keiner Gilde.<br>
Tritt einer Gilde bei oder gründe eine eigene.</div>
</div>`;
return;
}
const { guild, members, ranks, requests } = data;
const isLeader = (guild.leader_id === /* wird per Session gesetzt */ null);
// Mitglieder-Tabelle
const membersHtml = members.map(m => `
<tr>
<td>${m.name}</td>
<td>Lv. ${m.level}</td>
<td>
${guild.can_manage_ranks && m.id !== guild.leader_id
? `<select class="gh-rank-select" data-uid="${m.id}" onchange="ghChangeRank(${m.id},this.value)">
${ranks.map(r =>
`<option value="${r.id}" ${r.id === m.rank_id ? 'selected' : ''}>${r.name}</option>`
).join('')}
</select>`
: `<span class="gh-rank-badge ${m.id === guild.leader_id ? 'leader' : ''}">${m.rank_name || ''}</span>`
}
</td>
<td style="font-size:10px;color:var(--guild-muted)">
${new Date(m.joined_at).toLocaleDateString('de-DE')}
</td>
<td>
${guild.can_kick && m.id !== guild.leader_id
? `<button class="gh-kick-btn" onclick="ghKickMember(${m.id},'${m.name}')">Kick</button>`
: ''}
</td>
</tr>`).join('');
// Anfragen
const requestsHtml = requests.length > 0
? `<div class="gh-section-title">Beitrittsanfragen (${requests.length})</div>
<div class="gh-requests-list">
${requests.map(r => `
<div class="gh-request-row">
<span class="gh-request-name">${r.name}</span>
<span class="gh-request-msg">${r.message || ''}</span>
${guild.can_invite
? `<button class="gh-req-accept" onclick="ghHandleRequest(${r.id},'accept')">✔</button>
<button class="gh-req-reject" onclick="ghHandleRequest(${r.id},'reject')"></button>`
: ''}
</div>`).join('')}
</div>`
: '';
container.innerHTML = `
<div class="gh-my-info">
<span class="gh-my-tag">[${guild.tag}]</span>
<div>
<div class="gh-my-name">${guild.name}</div>
<div class="gh-my-sub">Lv. ${guild.level} · ${members.length}/${guild.max_members} Mitglieder</div>
</div>
<button class="gh-leave-btn" onclick="ghLeaveGuild()">Austreten</button>
</div>
${requestsHtml}
<div class="gh-section-title">Mitglieder</div>
<div style="padding:0 4px;overflow-x:auto;">
<table class="gh-members-table">
<thead>
<tr>
<th>Name</th><th>Level</th><th>Rang</th><th>Beigetreten</th><th></th>
</tr>
</thead>
<tbody>${membersHtml}</tbody>
</table>
</div>`;
} catch (err) {
container.innerHTML = '<div class="gh-empty">Fehler beim Laden.</div>';
console.error('[GH/my]', err);
}
}
/* ── Rang ändern ────────────────────────────────────────── */
window.ghChangeRank = async function(userId, rankId) {
try {
const res = await fetch(`/api/gildenhalle/member/${userId}/rank`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rank_id: rankId }),
});
if (!res.ok) { const d=await res.json(); alert(d.error||'Fehler'); ghLoadMyGuild(); }
else ghShowToast('✔ Rang geändert');
} catch {}
};
/* ── Mitglied kicken ────────────────────────────────────── */
window.ghKickMember = async function(userId, name) {
if (!confirm(`${name} aus der Gilde entfernen?`)) return;
try {
const res = await fetch(`/api/gildenhalle/member/${userId}/kick`, { method: 'POST' });
const d = await res.json();
if (!res.ok) { alert(d.error||'Fehler'); return; }
ghShowToast(`${name} wurde entfernt`);
ghLoadMyGuild();
} catch {}
};
/* ── Anfrage annehmen/ablehnen ──────────────────────────── */
window.ghHandleRequest = async function(requestId, action) {
try {
const res = await fetch(`/api/gildenhalle/requests/${requestId}/${action}`, { method: 'POST' });
const d = await res.json();
if (!res.ok) { alert(d.error||'Fehler'); return; }
ghShowToast(action === 'accept' ? '✔ Anfrage angenommen' : '✕ Anfrage abgelehnt');
ghLoadMyGuild();
} catch {}
};
/* ── Gilde verlassen ────────────────────────────────────── */
window.ghLeaveGuild = async function() {
if (!confirm('Wirklich die Gilde verlassen?')) return;
try {
const res = await fetch('/api/gildenhalle/leave', { method: 'POST' });
const d = await res.json();
if (!res.ok) { alert(d.error||'Fehler'); return; }
ghShowToast('Gilde verlassen');
ghLoadMyGuild();
gh_page = 1; ghLoadGuildList();
} catch {}
};
/*
PANEL 3: Aufgaben
*/
async function ghLoadTasks() {
const container = document.getElementById('gh-tasks-content');
if (!container) return;
container.innerHTML = '<div class="gh-loading">Lade Aufgaben…</div>';
try {
const res = await fetch('/api/gildenhalle/tasks');
if (res.status === 400) {
container.innerHTML = `
<div class="gh-no-guild">
<div class="gh-no-guild-icon">📋</div>
<div class="gh-no-guild-text">Tritt einer Gilde bei um gemeinsame Aufgaben zu sehen.</div>
</div>`;
return;
}
const data = await res.json();
if (!data.tasks?.length) {
container.innerHTML = '<div class="gh-empty">Keine Aufgaben für heute.</div>';
return;
}
const rewardIcons = { gold: '🪙', gems: '💎', wood: '🪵', iron: '⚙️' };
container.innerHTML = `
<div style="padding:10px 16px;font-family:'Cinzel',serif;font-size:11px;
color:var(--guild-muted);border-bottom:1px solid rgba(139,106,42,.25);">
Gemeinsame Aufgaben der Gilde <strong style="color:var(--guild-gold);">${data.guildName}</strong>
</div>
<div class="gh-tasks-list">
${data.tasks.map(t => {
const pct = Math.min(100, Math.round((t.current_amount / t.target_amount) * 100));
const done = t.completed || t.current_amount >= t.target_amount;
const icon = rewardIcons[t.reward_type] || '🪙';
return `
<div class="gh-task-card ${done ? 'completed' : ''}">
<div class="gh-task-header">
<span class="gh-task-label">${t.label}</span>
${done
? `<span class="gh-task-done-badge">✔ Erledigt</span>`
: `<span class="gh-task-reward">${icon} ${t.reward_amount} ${t.reward_type}</span>`}
</div>
<div class="gh-task-progress-wrap">
<div class="gh-task-bar-track">
<div class="gh-task-bar-fill ${done?'completed':''}"
style="width:${pct}%"></div>
</div>
<div class="gh-task-progress-text">
<span>${t.current_amount} / ${t.target_amount}</span>
<span>${pct}% · Mein Beitrag: ${t.my_contribution || 0}</span>
</div>
</div>
</div>`;
}).join('')}
</div>`;
} catch (err) {
container.innerHTML = '<div class="gh-empty">Fehler beim Laden.</div>';
console.error('[GH/tasks]', err);
}
}
/* ── Pagination ─────────────────────────────────────────── */
function ghRenderPagination(el, totalPages, total) {
if (!el || !totalPages || totalPages <= 1) return;
el.innerHTML = `
<button class="gh-page-btn" id="gh-prev" ${gh_page===1?'disabled':''}></button>
${Array.from({length:totalPages},(_,i)=>i+1).map(p =>
`<button class="gh-page-btn ${p===gh_page?'active':''}" data-page="${p}">${p}</button>`
).join('')}
<button class="gh-page-btn" id="gh-next" ${gh_page===totalPages?'disabled':''}></button>`;
el.querySelector('#gh-prev')?.addEventListener('click', () => { if(gh_page>1){gh_page--;ghLoadGuildList();} });
el.querySelector('#gh-next')?.addEventListener('click', () => { if(gh_page<totalPages){gh_page++;ghLoadGuildList();} });
el.querySelectorAll('[data-page]').forEach(btn =>
btn.addEventListener('click', () => { gh_page=parseInt(btn.dataset.page); ghLoadGuildList(); })
);
}
/* ── Toast ──────────────────────────────────────────────── */
function ghShowToast(msg) {
const t = document.createElement('div');
t.className = 'baz-toast'; t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 2800);
}
/* ── Schließen ──────────────────────────────────────────── */
function ghClose() {
document.getElementById('gildenhalle-popup')?.classList.remove('active');
document.getElementById('qm-overlay')?.classList.remove('active');
}
/*
EXPORT
*/
export function loadGildenhalle() {
ghLoadCSS();
ghEnsurePopup();
const popup = document.getElementById('gildenhalle-popup');
const overlay = document.getElementById('qm-overlay');
popup.style.left = '50%'; popup.style.top = '50%';
popup.style.transform = 'translate(-50%,-50%) scale(1)';
popup.classList.add('active');
overlay?.classList.add('active');
if (!gh_initialized) {
gh_initialized = true;
ghLoadGuildList();
}
}

View File

@ -5,6 +5,8 @@ import { loadBlackmarket } from "./buildings/blackmarket.js?v=2";
import { loadMine } from "./buildings/mine.js?v=2";
import { loadGlucksspiel } from "./gaststaette/glucksspiel.js";
import { loadBazaar } from "./buildings/bazaar.js";
import { loadGildenhalle } from "./buildings/gildenhalle.js";
const popup = document.getElementById("building-popup");
const title = document.getElementById("popup-title");
const tooltip = document.getElementById("map-tooltip");
@ -16,12 +18,16 @@ const tooltipCache = {};
================================ */
const gaststaettePopup = document.getElementById("gaststaette-popup");
const qmOverlay = document.getElementById("qm-overlay");
const qmOverlay = document.getElementById("qm-overlay");
document.querySelectorAll(".gaststaette-tab").forEach((tab) => {
tab.addEventListener("click", () => {
document.querySelectorAll(".gaststaette-tab").forEach((t) => t.classList.remove("active"));
document.querySelectorAll(".gaststaette-tab-content").forEach((c) => c.classList.remove("active"));
document
.querySelectorAll(".gaststaette-tab")
.forEach((t) => t.classList.remove("active"));
document
.querySelectorAll(".gaststaette-tab-content")
.forEach((c) => c.classList.remove("active"));
tab.classList.add("active");
document.getElementById(tab.dataset.tab).classList.add("active");
if (tab.dataset.tab === "gs-tab1") loadGlucksspiel();
@ -49,7 +55,7 @@ document.getElementById("qm-overlay")?.addEventListener("mouseenter", () => {
});
const buildingModules = {
1: loadArena, // Arena eigenes UI, Tabs ausblenden
1: loadArena, // Arena eigenes UI, Tabs ausblenden
3: loadHimmelstor, // Himmelstor eigenes UI, Tabs ausblenden
10: loadMine, // Mine Tabs bleiben sichtbar
11: loadCharacterHouse, // Charakterhaus eigenes UI, Tabs ausblenden
@ -182,7 +188,7 @@ document.querySelectorAll(".building").forEach((building) => {
// Gebäude 6 → Gaststätte eigenes Popup
if (url === "/building/6") {
gaststaettePopup.style.left = "50%";
gaststaettePopup.style.top = "50%";
gaststaettePopup.style.top = "50%";
gaststaettePopup.style.transform = "translate(-50%, -50%) scale(1)";
gaststaettePopup.classList.add("active");
qmOverlay.classList.add("active");
@ -196,6 +202,12 @@ document.querySelectorAll(".building").forEach((building) => {
return;
}
// Gebäude 9 → Gildenhalle eigenes Popup
if (url === "/building/9") {
loadGildenhalle();
return;
}
await openBuildingPopup(url);
});
});
@ -287,7 +299,8 @@ document.querySelectorAll(".building").forEach((building) => {
building.addEventListener("mouseenter", async (e) => {
try {
// data-id bevorzugen, sonst ID aus href extrahieren (z.B. "/building/5" → "5")
const id = building.dataset.id || building.getAttribute("href")?.split("/").pop();
const id =
building.dataset.id || building.getAttribute("href")?.split("/").pop();
if (!id) return;
if (!tooltipCache[id]) {

414
routes/gildenhalle.route.js Normal file
View File

@ -0,0 +1,414 @@
/* ============================================================
routes/gildenhalle.route.js
============================================================ */
'use strict';
const express = require('express');
const router = express.Router();
const db = require('../database/database');
function requireLogin(req, res, next) {
if (!req.session?.user) return res.status(401).json({ error: 'Nicht eingeloggt' });
next();
}
/* ── Hilfsfunktion: Spieler-Gilde laden ────────────────── */
async function getPlayerGuild(userId) {
const [[row]] = await db.query(
`SELECT g.*, gm.rank_id, gr.name AS rank_name, gr.sort_order,
gr.can_invite, gr.can_kick, gr.can_manage_ranks, gr.can_manage_tasks
FROM guild_members gm
JOIN guilds g ON g.id = gm.guild_id
LEFT JOIN guild_ranks gr ON gr.id = gm.rank_id
WHERE gm.user_id = ?`,
[userId]
);
return row || null;
}
/* ── Standard-Ränge für neue Gilde anlegen ─────────────── */
async function createDefaultRanks(guildId) {
const ranks = [
{ name: 'Gildenmeister', sort_order: 1, can_invite: 1, can_kick: 1, can_manage_ranks: 1, can_manage_tasks: 1 },
{ name: 'Offizier', sort_order: 2, can_invite: 1, can_kick: 1, can_manage_ranks: 0, can_manage_tasks: 1 },
{ name: 'Veteran', sort_order: 3, can_invite: 1, can_kick: 0, can_manage_ranks: 0, can_manage_tasks: 0 },
{ name: 'Mitglied', sort_order: 99, can_invite: 0, can_kick: 0, can_manage_ranks: 0, can_manage_tasks: 0 },
];
const ids = [];
for (const r of ranks) {
const [res] = await db.query(
`INSERT INTO guild_ranks (guild_id, name, sort_order, can_invite, can_kick, can_manage_ranks, can_manage_tasks)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[guildId, r.name, r.sort_order, r.can_invite, r.can_kick, r.can_manage_ranks, r.can_manage_tasks]
);
ids.push({ name: r.name, id: res.insertId });
}
return ids;
}
/* ── Tagesaufgaben für Gilde generieren ────────────────── */
async function ensureDailyTasks(guildId) {
const today = new Date().toISOString().slice(0, 10);
const [existing] = await db.query(
'SELECT id FROM guild_tasks WHERE guild_id = ? AND expires_at = ?',
[guildId, today]
);
if (existing.length > 0) return;
const [templates] = await db.query(
'SELECT * FROM guild_task_templates WHERE active = 1'
);
for (const t of templates) {
await db.query(
`INSERT IGNORE INTO guild_tasks
(guild_id, task_key, label, target_amount, reward_type, reward_amount, expires_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[guildId, t.task_key, t.label, t.target_amount, t.reward_type, t.reward_amount, today]
);
}
}
/*
GET /api/gildenhalle/list?search=&page=1
Alle Gilden auflisten
*/
router.get('/gildenhalle/list', requireLogin, async (req, res) => {
const search = req.query.search?.trim() || '';
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = 12;
const offset = (page - 1) * limit;
const userId = req.session.user.id;
try {
const searchParam = search ? `%${search}%` : '%';
const [guilds] = await db.query(
`SELECT g.id, g.name, g.tag, g.description, g.level, g.open, g.max_members,
COUNT(gm.id) AS member_count,
a.ingame_name AS leader_name
FROM guilds g
LEFT JOIN guild_members gm ON gm.guild_id = g.id
LEFT JOIN accounts a ON a.id = g.leader_id
WHERE g.name LIKE ? OR g.tag LIKE ?
GROUP BY g.id
ORDER BY g.level DESC, member_count DESC
LIMIT ? OFFSET ?`,
[searchParam, searchParam, limit, offset]
);
const [[{ total }]] = await db.query(
'SELECT COUNT(*) AS total FROM guilds WHERE name LIKE ? OR tag LIKE ?',
[searchParam, searchParam]
);
// Offene Anfrage des Spielers prüfen
const [myRequests] = await db.query(
"SELECT guild_id FROM guild_join_requests WHERE user_id = ? AND status = 'pending'",
[userId]
);
const pendingIds = new Set(myRequests.map(r => r.guild_id));
const playerGuild = await getPlayerGuild(userId);
res.json({
guilds: guilds.map(g => ({
...g,
has_pending_request: pendingIds.has(g.id),
})),
total,
totalPages: Math.ceil(total / limit),
page,
playerGuildId: playerGuild?.id || null,
});
} catch (err) {
console.error('[gildenhalle/list]', err);
res.status(500).json({ error: 'DB Fehler' });
}
});
/*
POST /api/gildenhalle/create
Neue Gilde gründen
*/
router.post('/gildenhalle/create', requireLogin, async (req, res) => {
const userId = req.session.user.id;
const { name, tag, description, open } = req.body;
if (!name?.trim() || !tag?.trim())
return res.status(400).json({ error: 'Name und Tag sind Pflichtfelder.' });
if (tag.length < 2 || tag.length > 6)
return res.status(400).json({ error: 'Tag muss 26 Zeichen lang sein.' });
try {
const existing = await getPlayerGuild(userId);
if (existing) return res.status(400).json({ error: 'Du bist bereits in einer Gilde.' });
const [result] = await db.query(
`INSERT INTO guilds (name, tag, description, leader_id, open)
VALUES (?, ?, ?, ?, ?)`,
[name.trim(), tag.trim().toUpperCase(), description?.trim() || null, userId, open ? 1 : 0]
);
const guildId = result.insertId;
// Standard-Ränge anlegen
const rankIds = await createDefaultRanks(guildId);
const leaderRankId = rankIds.find(r => r.name === 'Gildenmeister')?.id;
// Gründer als Mitglied mit höchstem Rang eintragen
await db.query(
'INSERT INTO guild_members (guild_id, user_id, rank_id) VALUES (?, ?, ?)',
[guildId, userId, leaderRankId]
);
// Erste Tagesaufgaben generieren
await ensureDailyTasks(guildId);
res.json({ success: true, guildId });
} catch (err) {
if (err.code === 'ER_DUP_ENTRY')
return res.status(400).json({ error: 'Gildenname oder Tag bereits vergeben.' });
console.error('[gildenhalle/create]', err);
res.status(500).json({ error: 'DB Fehler' });
}
});
/*
POST /api/gildenhalle/join/:id
Gilde beitreten oder Anfrage stellen
*/
router.post('/gildenhalle/join/:id', requireLogin, async (req, res) => {
const userId = req.session.user.id;
const guildId = parseInt(req.params.id);
try {
const existing = await getPlayerGuild(userId);
if (existing) return res.status(400).json({ error: 'Du bist bereits in einer Gilde.' });
const [[guild]] = await db.query('SELECT * FROM guilds WHERE id = ?', [guildId]);
if (!guild) return res.status(404).json({ error: 'Gilde nicht gefunden.' });
const [[memberCount]] = await db.query(
'SELECT COUNT(*) AS c FROM guild_members WHERE guild_id = ?', [guildId]
);
if (memberCount.c >= guild.max_members)
return res.status(400).json({ error: 'Gilde ist voll.' });
if (guild.open) {
// Standard-Rang (Mitglied = höchste sort_order)
const [[defaultRank]] = await db.query(
'SELECT id FROM guild_ranks WHERE guild_id = ? ORDER BY sort_order DESC LIMIT 1',
[guildId]
);
await db.query(
'INSERT INTO guild_members (guild_id, user_id, rank_id) VALUES (?, ?, ?)',
[guildId, userId, defaultRank?.id || null]
);
res.json({ success: true, joined: true });
} else {
// Anfrage stellen
await db.query(
`INSERT IGNORE INTO guild_join_requests (guild_id, user_id, message)
VALUES (?, ?, ?)`,
[guildId, userId, req.body.message?.trim() || null]
);
res.json({ success: true, joined: false, requested: true });
}
} catch (err) {
if (err.code === 'ER_DUP_ENTRY')
return res.status(400).json({ error: 'Anfrage bereits gestellt.' });
console.error('[gildenhalle/join]', err);
res.status(500).json({ error: 'DB Fehler' });
}
});
/*
POST /api/gildenhalle/leave
Gilde verlassen
*/
router.post('/gildenhalle/leave', requireLogin, async (req, res) => {
const userId = req.session.user.id;
try {
const guild = await getPlayerGuild(userId);
if (!guild) return res.status(400).json({ error: 'Du bist in keiner Gilde.' });
if (guild.leader_id === userId)
return res.status(400).json({ error: 'Als Gildenleiterin kannst du nicht austreten. Übertrage zuerst die Leitung.' });
await db.query('DELETE FROM guild_members WHERE user_id = ? AND guild_id = ?', [userId, guild.id]);
res.json({ success: true });
} catch (err) {
console.error('[gildenhalle/leave]', err);
res.status(500).json({ error: 'DB Fehler' });
}
});
/*
GET /api/gildenhalle/my
Eigene Gilde + Mitglieder laden
*/
router.get('/gildenhalle/my', requireLogin, async (req, res) => {
const userId = req.session.user.id;
try {
const guild = await getPlayerGuild(userId);
if (!guild) return res.json({ guild: null });
await ensureDailyTasks(guild.id);
const [members] = await db.query(
`SELECT a.id, a.ingame_name AS name, a.level,
gr.id AS rank_id, gr.name AS rank_name, gr.sort_order,
gm.joined_at
FROM guild_members gm
JOIN accounts a ON a.id = gm.user_id
LEFT JOIN guild_ranks gr ON gr.id = gm.rank_id
WHERE gm.guild_id = ?
ORDER BY gr.sort_order ASC, gm.joined_at ASC`,
[guild.id]
);
const [ranks] = await db.query(
'SELECT * FROM guild_ranks WHERE guild_id = ? ORDER BY sort_order ASC',
[guild.id]
);
const [requests] = await db.query(
`SELECT gjr.id, gjr.user_id, a.ingame_name AS name, gjr.message, gjr.created_at
FROM guild_join_requests gjr
JOIN accounts a ON a.id = gjr.user_id
WHERE gjr.guild_id = ? AND gjr.status = 'pending'`,
[guild.id]
);
res.json({ guild, members, ranks, requests });
} catch (err) {
console.error('[gildenhalle/my]', err);
res.status(500).json({ error: 'DB Fehler' });
}
});
/*
GET /api/gildenhalle/tasks
Gilden-Tagesaufgaben laden
*/
router.get('/gildenhalle/tasks', requireLogin, async (req, res) => {
const userId = req.session.user.id;
try {
const guild = await getPlayerGuild(userId);
if (!guild) return res.status(400).json({ error: 'Du bist in keiner Gilde.' });
await ensureDailyTasks(guild.id);
const today = new Date().toISOString().slice(0, 10);
const [tasks] = await db.query(
`SELECT t.*,
COALESCE(SUM(c.amount), 0) AS contributed_total,
(SELECT COALESCE(SUM(c2.amount),0) FROM guild_task_contributions c2
WHERE c2.task_id = t.id AND c2.user_id = ?) AS my_contribution
FROM guild_tasks t
LEFT JOIN guild_task_contributions c ON c.task_id = t.id
WHERE t.guild_id = ? AND t.expires_at = ?
GROUP BY t.id`,
[userId, guild.id, today]
);
res.json({ tasks, guildName: guild.name });
} catch (err) {
console.error('[gildenhalle/tasks]', err);
res.status(500).json({ error: 'DB Fehler' });
}
});
/*
POST /api/gildenhalle/requests/:id/accept
POST /api/gildenhalle/requests/:id/reject
Beitrittsanfragen verwalten
*/
router.post('/gildenhalle/requests/:id/:action', requireLogin, async (req, res) => {
const userId = req.session.user.id;
const requestId = parseInt(req.params.id);
const action = req.params.action; // accept | reject
if (!['accept', 'reject'].includes(action))
return res.status(400).json({ error: 'Ungültige Aktion.' });
try {
const guild = await getPlayerGuild(userId);
if (!guild?.can_invite) return res.status(403).json({ error: 'Keine Berechtigung.' });
const [[request]] = await db.query(
"SELECT * FROM guild_join_requests WHERE id = ? AND guild_id = ? AND status = 'pending'",
[requestId, guild.id]
);
if (!request) return res.status(404).json({ error: 'Anfrage nicht gefunden.' });
await db.query(
'UPDATE guild_join_requests SET status = ? WHERE id = ?',
[action === 'accept' ? 'accepted' : 'rejected', requestId]
);
if (action === 'accept') {
const [[defaultRank]] = await db.query(
'SELECT id FROM guild_ranks WHERE guild_id = ? ORDER BY sort_order DESC LIMIT 1',
[guild.id]
);
await db.query(
'INSERT IGNORE INTO guild_members (guild_id, user_id, rank_id) VALUES (?, ?, ?)',
[guild.id, request.user_id, defaultRank?.id || null]
);
}
res.json({ success: true });
} catch (err) {
console.error('[gildenhalle/requests]', err);
res.status(500).json({ error: 'DB Fehler' });
}
});
/*
POST /api/gildenhalle/member/:userId/rank
Rang eines Mitglieds ändern
*/
router.post('/gildenhalle/member/:targetId/rank', requireLogin, async (req, res) => {
const userId = req.session.user.id;
const targetId = parseInt(req.params.targetId);
const { rank_id } = req.body;
try {
const guild = await getPlayerGuild(userId);
if (!guild?.can_manage_ranks) return res.status(403).json({ error: 'Keine Berechtigung.' });
if (targetId === guild.leader_id) return res.status(403).json({ error: 'Rang des Gildenmeisters kann nicht geändert werden.' });
await db.query(
'UPDATE guild_members SET rank_id = ? WHERE user_id = ? AND guild_id = ?',
[rank_id, targetId, guild.id]
);
res.json({ success: true });
} catch (err) {
console.error('[gildenhalle/member/rank]', err);
res.status(500).json({ error: 'DB Fehler' });
}
});
/*
POST /api/gildenhalle/member/:userId/kick
Mitglied entfernen
*/
router.post('/gildenhalle/member/:targetId/kick', requireLogin, async (req, res) => {
const userId = req.session.user.id;
const targetId = parseInt(req.params.targetId);
try {
const guild = await getPlayerGuild(userId);
if (!guild?.can_kick) return res.status(403).json({ error: 'Keine Berechtigung.' });
if (targetId === guild.leader_id) return res.status(403).json({ error: 'Gildenmeister kann nicht gekickt werden.' });
await db.query(
'DELETE FROM guild_members WHERE user_id = ? AND guild_id = ?',
[targetId, guild.id]
);
res.json({ success: true });
} catch (err) {
console.error('[gildenhalle/member/kick]', err);
res.status(500).json({ error: 'DB Fehler' });
}
});
module.exports = router;

View File

@ -97,6 +97,7 @@
<link rel="stylesheet" href="/css/daily.css" />
<link rel="stylesheet" href="/css/events.css" />
<link rel="stylesheet" href="/css/hud.css" />
<link rel="stylesheet" href="/css/gildenhalle.css" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"