dok/public/js/buildings/gildenhalle.js
2026-04-14 18:50:24 +01:00

548 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
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:#7a4a00;color:#7a4a00;">+ 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:#5a3a18">
${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:#5a3a18;border-bottom:1px solid rgba(139,106,42,.25);">
Gemeinsame Aufgaben der Gilde <strong style="color:#7a4a00;">${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();
}
}