dok/public/js/quickmenu/carddeck.js
2026-04-11 12:37:10 +01:00

1036 lines
36 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.

const CARDS_PER_PAGE = 18;
let currentGroupId = null;
let currentPage = 1;
let currentDeckId = null;
let deckCards = []; // [{card_id, amount}]
let userCardsCache = []; // aktuelle Seite: [{card_id, amount, name, image, rarity, attack, defense, range, race}]
let decks = []; // [{id, name}]
/* ── Kristall-Mapping ───────────────────────── */
const RARITY_CRYSTALS = {
1: "roter-cristal.png",
2: "blauer-cristal.png",
3: "gelber-cristal.png",
4: "gruener-cristal.png",
5: "oranger-cristal.png",
6: "violet-cristal.png",
7: "pinker-cristal.png",
};
function rarityImgs(rarity, size = 14) {
const file = RARITY_CRYSTALS[String(rarity)];
if (!file) return "";
const count = parseInt(rarity) || 0;
const img = `<img src="/images/items/${file}" alt="Stufe ${rarity}" style="width:${size}px;height:${size}px;object-fit:contain;filter:drop-shadow(0 1px 2px rgba(0,0,0,0.8));">`;
return img.repeat(count);
}
/* ── Stat-Icons ─────────────────────────────── */
function rangeIcon() {
return `<svg viewBox="0 0 16 16" width="11" height="11" style="display:inline;vertical-align:middle;flex-shrink:0" fill="none" stroke="#e8b84b" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 2 Q1 8 4 14"/>
<line x1="4" y1="2" x2="4" y2="14" stroke-width="0.7" stroke-dasharray="2,1.5"/>
<line x1="4" y1="8" x2="13" y2="8"/>
<polyline points="11,6 13,8 11,10"/>
<line x1="5" y1="7" x2="4" y2="8"/><line x1="5" y1="9" x2="4" y2="8"/>
</svg>`;
}
function raceIcon() {
return `<svg viewBox="0 0 16 16" width="11" height="11" style="display:inline;vertical-align:middle;flex-shrink:0" fill="none" stroke="#7de87d" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="9" cy="2.5" r="1.4" fill="#7de87d" stroke="none"/>
<line x1="9" y1="4" x2="8" y2="9"/>
<line x1="8" y1="9" x2="10" y2="14"/>
<line x1="8" y1="9" x2="6" y2="13"/>
<line x1="8.5" y1="5.5" x2="11" y2="8"/>
<line x1="8.5" y1="5.5" x2="6" y2="7"/>
</svg>`;
}
/* ══════════════════════════════════════════════
INIT
══════════════════════════════════════════════ */
export async function loadCardDeck() {
const body = document.getElementById("qm-body-carddeck");
if (!body) return;
currentDeckId = null;
deckCards = [];
body.innerHTML = renderShell();
attachShellEvents();
try {
const res = await fetch("/api/card-groups");
if (!res.ok) throw new Error("API Fehler");
const groups = await res.json();
renderTabs(groups);
if (groups.length) {
currentGroupId = groups[0].id;
await Promise.all([loadUserCards(), loadDecks()]);
}
} catch (err) {
console.error("Init Fehler:", err);
}
}
/* ══════════════════════════════════════════════
SHELL HTML
══════════════════════════════════════════════ */
function renderShell() {
return `
<div class="kd-wrap">
<!-- Linke Tab-Leiste -->
<aside class="kd-tabs" id="kd-tabs">
<div class="kd-loading">Lade...</div>
</aside>
<!-- Hauptbereich: Sammlung + Deck-Builder -->
<div class="kd-split">
<!-- LINKE SPALTE: Kartensammlung -->
<div class="kd-collection">
<div class="kd-col-header">Meine Karten</div>
<div class="kd-grid" id="kd-grid">
<div class="kd-loading">Lade Karten...</div>
</div>
<div class="kd-pagination" id="kd-pagination"></div>
</div>
<!-- RECHTE SPALTE: Deck-Builder -->
<div class="kd-deckbuilder">
<div class="kd-col-header">Deck-Builder</div>
<!-- Deck Auswahl / Erstellen -->
<div class="kd-deck-controls">
<select class="kd-deck-select" id="kd-deck-select">
<option value="">— Deck wählen —</option>
</select>
<button class="kd-btn-new" id="kd-btn-new-deck" title="Neues Deck erstellen"></button>
</div>
<!-- Deck Info -->
<div class="kd-deck-info" id="kd-deck-info"></div>
<!-- Karten im Deck -->
<div class="kd-deck-grid" id="kd-deck-grid">
<div class="kd-empty-deck">Kein Deck ausgewählt.</div>
</div>
</div>
</div>
</div>
<style>
/* ── Layout ─────────────────────────────── */
.kd-wrap {
display: flex;
width: 100%;
height: 100%;
font-family: "Cinzel", serif;
overflow: hidden;
}
.kd-tabs {
display: flex;
flex-direction: column;
gap: 6px;
padding: 16px 10px;
background: rgba(0,0,0,0.3);
border-right: 2px solid #6b4b2a;
min-width: 150px;
flex-shrink: 0;
overflow-y: auto;
}
.kd-split {
flex: 1;
display: flex;
overflow: hidden;
}
/* ── Linke Spalte: Sammlung ─────────────── */
.kd-collection {
flex: 1;
display: flex;
flex-direction: column;
border-right: 2px solid #6b4b2a;
overflow: hidden;
padding: 0;
}
/* ── Rechte Spalte: Deck-Builder ─────────── */
.kd-deckbuilder {
width: 500px;
flex-shrink: 0;
display: flex;
flex-direction: column;
overflow: hidden;
background: rgba(0,0,0,0.15);
}
/* ── Spalten-Header ─────────────────────── */
.kd-col-header {
font-family: "Cinzel", serif;
font-size: 15px;
font-weight: 700;
color: #f0d9a6;
letter-spacing: 1px;
text-transform: uppercase;
padding: 10px 16px;
background: linear-gradient(#3a2810cc, #1a0f04cc);
border-bottom: 2px solid #6b4b2a;
flex-shrink: 0;
}
/* ── Tabs ────────────────────────────────── */
.kd-tab {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 12px;
background: linear-gradient(135deg, #2a1a08, #1a0f04);
border: 2px solid #6b4b2a;
border-radius: 8px;
color: #c8a86a;
font-family: "Cinzel", serif;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
text-align: left;
white-space: nowrap;
}
.kd-tab:hover { color: #f0d9a6; filter: brightness(1.2); }
.kd-tab-active {
color: #fff !important;
box-shadow: inset 0 0 10px rgba(0,0,0,0.5), 0 0 14px rgba(200,160,60,0.3) !important;
}
.kd-tab-dot {
width: 10px; height: 10px;
border-radius: 50%; flex-shrink: 0;
border: 1px solid rgba(255,255,255,0.3);
}
/* ── Karten-Grid (Sammlung) ──────────────── */
.kd-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 10px;
overflow-y: auto;
padding: 12px 14px;
align-content: start;
}
.kd-card {
position: relative;
border: 2px solid #6b4b2a;
border-radius: 8px;
overflow: visible;
background: #1a0f04;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
aspect-ratio: 3/4;
display: flex;
flex-direction: column;
}
.kd-card:hover {
transform: scale(1.06) translateY(-4px);
border-color: #f0d060;
box-shadow: 0 8px 24px rgba(0,0,0,0.7), 0 0 14px rgba(200,160,60,0.3);
z-index: 10;
}
.kd-card.kd-card-maxed {
border-color: #555;
opacity: 0.5;
cursor: not-allowed;
}
.kd-card.kd-card-maxed:hover {
transform: none;
box-shadow: none;
border-color: #555;
}
.kd-card img {
width: 100%; height: 100%;
object-fit: cover;
display: block;
border-radius: 6px 6px 0 0;
}
.kd-card-name {
position: absolute;
top: 0; left: 0; right: 0;
padding: 3px 5px;
background: linear-gradient(rgba(0,0,0,0.75), transparent);
font-family: "Cinzel", serif;
font-size: 9px;
color: #f0d9a6;
text-align: center;
line-height: 1.3;
border-radius: 6px 6px 0 0;
}
.kd-stat-atk {
position: absolute;
right: 12px;
top: 40%;
transform: translateY(-50%);
background: rgba(180,40,20,0.88);
border: 1px solid #ff6040;
border-radius: 45px; color: #fff; font-family: "Cinzel", serif;
font-size: 10px; font-weight: bold; padding: 2px 5px;
z-index: 5;
}
.kd-stat-def {
position: absolute;
left: 12px;
top: 40%;
transform: translateY(-50%);
background: rgba(20,80,180,0.88);
border: 1px solid #4090ff;
border-radius: 45px; color: #fff; font-family: "Cinzel", serif;
font-size: 10px; font-weight: bold; padding: 2px 5px;
z-index: 5;
}
.kd-stat-cd {
position: absolute;
bottom: 7px;
right: 11px;
width: 22px; height: 22px;
border-radius: 50%;
background: rgba(0,0,0,0.75);
border: 1px solid #f0d060;
display: flex; align-items: center; justify-content: center;
font-family: "Cinzel", serif; font-size: 9px; font-weight: bold;
color: #f0d9a6;
z-index: 5;
}
/* ── Karten-Footer: Anzahlen ────────────── */
.kd-card-footer {
position: absolute;
bottom: 4px;
left: 6px;
display: flex;
align-items: center;
background: transparent;
z-index: 5;
pointer-events: none;
}
.kd-count-owned {
font-family: "Cinzel", serif;
font-size: 13px;
color: #000;
font-weight: bold;
text-shadow: 0 0 3px rgba(255,255,255,0.6);
}
.kd-count-deck {
font-family: "Cinzel", serif;
font-size: 10px;
color: #88ccff;
font-weight: bold;
}
/* ── Pagination ──────────────────────────── */
.kd-pagination {
display: flex; align-items: center; justify-content: center;
gap: 6px; padding: 10px 14px; flex-shrink: 0;
border-top: 1px solid #3a2810;
}
.kd-page-btn {
background: linear-gradient(#3a2810, #1a0f04);
border: 1px solid #8b6a3c; border-radius: 5px;
color: #f0d9a6; font-family: "Cinzel", serif;
font-size: 11px; padding: 4px 11px; cursor: pointer; transition: 0.15s;
}
.kd-page-btn:hover { border-color: #f0d060; }
.kd-page-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.kd-page-btn.kd-page-active {
background: linear-gradient(#6b4b2a, #3c2414);
border-color: #f0d060; color: #fff5cc;
}
.kd-page-info { color: #a08060; font-size: 11px; min-width: 70px; text-align: center; }
/* ── Deck-Controls ───────────────────────── */
.kd-deck-controls {
display: flex; gap: 8px; padding: 10px 12px;
border-bottom: 1px solid #3a2810; flex-shrink: 0;
}
.kd-deck-select {
flex: 1;
background: linear-gradient(#2a1a08, #1a0f04);
border: 2px solid #6b4b2a; border-radius: 7px;
color: #ffffff; font-family: "Cinzel", serif; font-size: 12px;
padding: 6px 10px; cursor: pointer; outline: none;
}
.kd-deck-select:focus { border-color: #f0d060; }
.kd-btn-new {
background: linear-gradient(#4a3018, #2a1a08);
border: 2px solid #8b6a3c; border-radius: 7px;
color: #f0d9a6; font-size: 18px; width: 36px; height: 36px;
cursor: pointer; transition: 0.2s; display: flex; align-items: center; justify-content: center;
}
.kd-btn-new:hover { border-color: #f0d060; color: #fff; }
/* ── Deck Info Bar ───────────────────────── */
.kd-deck-info {
padding: 6px 12px;
font-family: "Cinzel", serif; font-size: 11px; color: #a08060;
border-bottom: 1px solid #3a2810; flex-shrink: 0;
display: flex; justify-content: space-between; align-items: center;
min-height: 28px;
}
.kd-deck-count {
font-weight: bold;
color: #f0d9a6;
}
.kd-deck-count.full { color: #ff6666; }
/* ── Deck-Grid (Karten im Deck) ──────────── */
.kd-deck-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
overflow-y: auto;
padding: 10px 12px;
align-content: start;
}
.kd-deck-card {
position: relative;
border: 2px solid #4a6b2a;
border-radius: 8px;
overflow: visible;
background: #0f1a04;
cursor: pointer;
transition: transform 0.2s, border-color 0.2s;
aspect-ratio: 3/4;
display: flex;
flex-direction: column;
}
.kd-deck-card:hover {
border-color: #ff4444;
transform: scale(1.05);
z-index: 10;
}
.kd-deck-card:hover::after {
content: "✕";
position: absolute;
inset: 0;
background: rgba(180,0,0,0.55);
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: #fff;
border-radius: 6px;
pointer-events: none;
}
.kd-deck-card img {
width: 100%; height: 100%;
object-fit: cover; display: block;
border-radius: 6px 6px 0 0;
}
.kd-deck-card-footer {
display: flex;
justify-content: center;
align-items: center;
padding: 2px 5px;
background: rgba(0,0,0,0.85);
border-top: 1px solid #2a3a10;
border-radius: 0 0 6px 6px;
height: 20px;
flex-shrink: 0;
}
.kd-deck-card-name {
font-family: "Cinzel", serif; font-size: 9px;
color: #c8e0a6; text-align: center;
}
.kd-deck-card-footer-counts {
position: absolute;
bottom: 4px;
left: 6px;
display: flex;
align-items: center;
background: transparent;
z-index: 5;
pointer-events: none;
}
.kd-deck-count-indeck { font-family: "Cinzel", serif; font-size: 13px; color: #000; font-weight: bold; text-shadow: 0 0 3px rgba(255,255,255,0.6); }
/* ── Reichweite & Laufen Badges ────────── */
.kd-range-race {
position: absolute;
bottom: 20px;
left: 0; right: 0;
display: flex;
justify-content: center;
gap: 4px;
padding: 0 3px;
pointer-events: none;
z-index: 6;
}
.kd-badge-range, .kd-badge-race {
display: flex;
align-items: center;
gap: 2px;
padding: 1px 4px;
border-radius: 20px;
font-family: "Cinzel", serif;
font-size: 9px;
font-weight: bold;
line-height: 1;
}
.kd-badge-range {
background: rgba(30,20,0,0.82);
border: 1px solid #e8b84b;
color: #e8b84b;
}
.kd-badge-race {
background: rgba(0,25,0,0.82);
border: 1px solid #7de87d;
color: #7de87d;
}
/* ── Rarity Kristalle ───────────────────── */
.kd-rarity {
position: absolute;
top: 75%;
left: 0; right: 0;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 1px;
padding: 0 4px;
pointer-events: none;
z-index: 4;
}
/* ── Empty / Loading States ──────────────── */
.kd-empty {
grid-column: 1 / -1; text-align: center;
color: #8b6a3c; font-size: 14px; padding: 40px 0;
}
.kd-empty-deck {
grid-column: 1 / -1; text-align: center;
color: #5a4a2a; font-size: 13px; padding: 30px 10px; line-height: 1.8;
}
.kd-loading {
text-align: center; color: #8b6a3c; font-size: 13px;
padding: 20px 0; animation: kd-pulse 1.2s infinite;
}
@keyframes kd-pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
/* ── Modal: Neues Deck ───────────────────── */
.kd-modal-overlay {
position: absolute; inset: 0;
background: rgba(0,0,0,0.7);
display: flex; align-items: center; justify-content: center;
z-index: 100; border-radius: 0 0 10px 10px;
}
.kd-modal {
background: linear-gradient(#2a1a08, #1a0f04);
border: 2px solid #8b6a3c; border-radius: 10px;
padding: 24px 28px; min-width: 300px;
display: flex; flex-direction: column; gap: 14px;
}
.kd-modal h3 {
font-family: "Cinzel", serif; font-size: 16px;
color: #f0d9a6; margin: 0;
}
.kd-modal input {
background: rgba(0,0,0,0.4); border: 2px solid #6b4b2a;
border-radius: 6px; color: #f0d9a6;
font-family: "Cinzel", serif; font-size: 13px;
padding: 8px 12px; outline: none;
}
.kd-modal input:focus { border-color: #f0d060; }
.kd-modal-btns { display: flex; gap: 10px; justify-content: flex-end; }
.kd-modal-btn {
background: linear-gradient(#4a3018, #2a1a08);
border: 2px solid #8b6a3c; border-radius: 6px;
color: #f0d9a6; font-family: "Cinzel", serif;
font-size: 12px; padding: 6px 16px; cursor: pointer; transition: 0.2s;
}
.kd-modal-btn:hover { border-color: #f0d060; color: #fff; }
.kd-modal-btn.primary {
background: linear-gradient(#6b4b2a, #3c2414);
border-color: #f0d060;
}
</style>
`;
}
/* ══════════════════════════════════════════════
TABS
══════════════════════════════════════════════ */
function renderTabs(groups) {
const container = document.getElementById("kd-tabs");
if (!container) return;
container.innerHTML = groups
.map((g, i) => {
const color = g.color || "#6b4b2a";
return `
<button class="kd-tab ${i === 0 ? "kd-tab-active" : ""}"
data-group="${g.id}"
style="border-color:${color};${i === 0 ? `background:${color}33;` : ""}">
<span class="kd-tab-dot" style="background:${color};"></span>
<span>${g.name}</span>
</button>`;
})
.join("");
container.querySelectorAll(".kd-tab").forEach((btn) => {
btn.addEventListener("click", async () => {
const color = btn.style.borderColor;
container.querySelectorAll(".kd-tab").forEach((b) => {
b.classList.remove("kd-tab-active");
b.style.background = "";
});
btn.classList.add("kd-tab-active");
btn.style.background = `${color}33`;
currentGroupId = parseInt(btn.dataset.group);
currentPage = 1;
await loadUserCards();
});
});
}
/* ══════════════════════════════════════════════
USER-KARTEN laden
GET /api/user-cards?group_id=X&page=Y&limit=Z
→ { cards: [{card_id, level, amount, name, image, attack, defense}],
totalPages, total }
══════════════════════════════════════════════ */
async function loadUserCards() {
const grid = document.getElementById("kd-grid");
const pagination = document.getElementById("kd-pagination");
if (!grid) return;
grid.innerHTML = `<div class="kd-loading">Lade Karten...</div>`;
pagination.innerHTML = "";
try {
const res = await fetch(
`/api/user-cards?group_id=${currentGroupId}&page=${currentPage}&limit=${CARDS_PER_PAGE}`,
);
if (!res.ok) throw new Error("API Fehler");
const data = await res.json();
userCardsCache = data.cards || [];
renderCollectionGrid(grid, userCardsCache);
renderPagination(pagination, data.totalPages, data.total);
} catch {
grid.innerHTML = `<div class="kd-empty">Keine Karten gefunden.</div>`;
}
}
/* ══════════════════════════════════════════════
DECKS laden
GET /api/decks
→ [{id, name, card_count}]
══════════════════════════════════════════════ */
async function loadDecks() {
try {
const res = await fetch("/api/decks");
if (!res.ok) throw new Error("API Fehler");
decks = await res.json();
renderDeckSelect();
} catch (err) {
console.error("Decks Fehler:", err);
}
}
function renderDeckSelect() {
const sel = document.getElementById("kd-deck-select");
if (!sel) return;
sel.innerHTML =
`<option value="">— Deck wählen —</option>` +
decks
.map(
(d) =>
`<option value="${d.id}" ${d.id === currentDeckId ? "selected" : ""}>${d.name}</option>`,
)
.join("");
sel.value = currentDeckId || "";
}
/* ══════════════════════════════════════════════
DECK-KARTEN laden
GET /api/decks/:id/cards
→ [{card_id, level, amount, name, image, attack, defense}]
══════════════════════════════════════════════ */
async function loadDeckCards(deckId) {
const grid = document.getElementById("kd-deck-grid");
const info = document.getElementById("kd-deck-info");
if (!grid) return;
grid.innerHTML = `<div class="kd-loading">Lade Deck...</div>`;
info.innerHTML = "";
try {
const res = await fetch(`/api/decks/${deckId}/cards`);
if (!res.ok) throw new Error("API Fehler");
deckCards = await res.json();
const total = deckCards.reduce((s, c) => s + c.amount, 0);
const isFull = total >= 30;
info.innerHTML = `
<span>Karten im Deck:</span>
<span class="kd-deck-count ${isFull ? "full" : ""}">${total} / 30</span>`;
renderDeckGrid(grid, deckCards);
// Sammlung neu rendern damit Deck-Zähler aktualisiert werden
renderCollectionGrid(document.getElementById("kd-grid"), userCardsCache);
} catch {
grid.innerHTML = `<div class="kd-empty">Fehler beim Laden des Decks.</div>`;
}
}
/* ══════════════════════════════════════════════
KARTE ZUM DECK HINZUFÜGEN
POST /api/decks/:id/cards { card_id, level }
══════════════════════════════════════════════ */
async function addCardToDeck(card) {
if (!currentDeckId) {
showToast("Bitte zuerst ein Deck auswählen.");
return;
}
const totalInDeck = deckCards.reduce((s, c) => s + c.amount, 0);
if (totalInDeck >= 30) {
showToast("Deck ist voll! (max. 30 Karten)");
return;
}
// Rarity > 5: max. 1 Exemplar im Deck
if (parseInt(card.rarity) > 5) {
const already = deckCards.find((c) => c.card_id === card.card_id);
if (already) {
showToast("Karten ab Rarity 6 dürfen nur 1x im Deck sein.");
return;
}
}
// Nicht mehr als vorhanden besitzt
const owned = card.amount;
const inDeck = getDeckCount(card.card_id);
if (inDeck >= owned) {
showToast("Du hast keine weiteren Exemplare dieser Karte.");
return;
}
try {
const res = await fetch(`/api/decks/${currentDeckId}/cards`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ card_id: card.card_id }),
});
if (!res.ok) {
const err = await res.json();
showToast(err.message || "Fehler beim Hinzufügen.");
return;
}
await loadDeckCards(currentDeckId);
} catch {
showToast("Verbindungsfehler.");
}
}
/* ══════════════════════════════════════════════
KARTE AUS DECK ENTFERNEN
DELETE /api/decks/:id/cards { card_id, level }
══════════════════════════════════════════════ */
async function removeCardFromDeck(card) {
try {
const res = await fetch(`/api/decks/${currentDeckId}/cards`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ card_id: card.card_id }),
});
if (!res.ok) {
const err = await res.json();
showToast(err.message || "Fehler beim Entfernen.");
return;
}
await loadDeckCards(currentDeckId);
} catch {
showToast("Verbindungsfehler.");
}
}
/* ══════════════════════════════════════════════
NEUES DECK ERSTELLEN
POST /api/decks { name }
══════════════════════════════════════════════ */
async function createDeck(name) {
try {
const res = await fetch("/api/decks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
});
if (!res.ok) {
const err = await res.json();
showToast(err.message || "Fehler beim Erstellen.");
return;
}
const deck = await res.json();
await loadDecks();
// Neues Deck direkt auswählen
currentDeckId = deck.id;
deckCards = [];
renderDeckSelect();
// Deck-Grid leeren + Info setzen
document.getElementById("kd-deck-grid").innerHTML =
`<div class="kd-empty-deck">Deck ist leer.<br>Klicke links auf eine Karte um sie hinzuzufügen.</div>`;
document.getElementById("kd-deck-info").innerHTML =
`<span>Karten im Deck:</span><span class="kd-deck-count">0 / 30</span>`;
renderCollectionGrid(document.getElementById("kd-grid"), userCardsCache);
} catch {
showToast("Verbindungsfehler.");
}
}
/* ══════════════════════════════════════════════
RENDER: SAMMLUNGS-GRID
══════════════════════════════════════════════ */
function renderCollectionGrid(grid, cards) {
if (!grid) return;
if (!cards || !cards.length) {
grid.innerHTML = `<div class="kd-empty">Keine Karten in dieser Gruppe.</div>`;
return;
}
const totalInDeck = deckCards.reduce((s, c) => s + c.amount, 0);
grid.innerHTML = cards
.map((c) => {
const inDeck = getDeckCount(c.card_id);
const maxed = isMaxedOut(c, inDeck, totalInDeck);
return `
<div class="kd-card ${maxed ? "kd-card-maxed" : ""}"
data-card-id="${c.card_id}"
title="${c.name}">
<img src="/images/cards/${c.image}" alt="${c.name}"
onerror="this.src='/images/avatar_placeholder.svg'">
${c.attack != null ? `<span class="kd-stat-atk">${c.attack}</span>` : ""}
${c.defends != null ? `<span class="kd-stat-def">${c.defends}</span>` : ""}
${c.cooldown != null ? `<span class="kd-stat-cd">${c.cooldown}</span>` : ""}
<div class="kd-card-footer">
<span class="kd-count-owned" title="Besitzt du">× ${c.amount}</span>
</div>
${c.rarity ? `<div class="kd-rarity">${rarityImgs(c.rarity, 13)}</div>` : ""}
${(c.range != null || c.race != null) ? `
<div class="kd-range-race">
${c.range != null ? `<span class="kd-badge-range">${rangeIcon()}&thinsp;${c.range}</span>` : ""}
${c.race != null ? `<span class="kd-badge-race">${raceIcon()}&thinsp;${c.race}</span>` : ""}
</div>` : ""}
</div>`;
})
.join("");
grid.querySelectorAll(".kd-card:not(.kd-card-maxed)").forEach((el) => {
el.addEventListener("click", async () => {
const card = cards.find((c) => c.card_id === parseInt(el.dataset.cardId));
if (card) await addCardToDeck(card);
});
});
}
/* ══════════════════════════════════════════════
RENDER: DECK-GRID
══════════════════════════════════════════════ */
function renderDeckGrid(grid, cards) {
if (!cards || !cards.length) {
grid.innerHTML = `<div class="kd-empty-deck">Deck ist leer.<br>Klicke links auf eine Karte um sie hinzuzufügen.</div>`;
return;
}
// Owned-Anzahl aus userCardsCache auslesen
grid.innerHTML = cards
.map((c) => {
const ownedEntry = userCardsCache.find((u) => u.card_id === c.card_id);
const ownedAmt = ownedEntry ? ownedEntry.amount : "?";
return `
<div class="kd-deck-card" data-card-id="${c.card_id}" title="Klicken zum Entfernen: ${c.name}">
<img src="/images/cards/${c.image}" alt="${c.name}"
onerror="this.src='/images/avatar_placeholder.svg'">
${c.attack != null ? `<span class="kd-stat-atk">${c.attack}</span>` : ""}
${c.defends != null ? `<span class="kd-stat-def">${c.defends}</span>` : ""}
${c.cooldown != null ? `<span class="kd-stat-cd">${c.cooldown}</span>` : ""}
<div class="kd-deck-card-footer-counts">
<span class="kd-deck-count-indeck" title="Im Deck">🃏 ${c.amount}</span>
</div>
${c.rarity ? `<div class="kd-rarity" style="top:75%">${rarityImgs(c.rarity, 14)}</div>` : ""}
${(c.range != null || c.race != null) ? `
<div class="kd-range-race">
${c.range != null ? `<span class="kd-badge-range">${rangeIcon()}&thinsp;${c.range}</span>` : ""}
${c.race != null ? `<span class="kd-badge-race">${raceIcon()}&thinsp;${c.race}</span>` : ""}
</div>` : ""}
</div>`;
})
.join("");
grid.querySelectorAll(".kd-deck-card").forEach((el) => {
el.addEventListener("click", async () => {
const card = cards.find((c) => c.card_id === parseInt(el.dataset.cardId));
if (card) await removeCardFromDeck(card);
});
});
}
/* ══════════════════════════════════════════════
PAGINATION
══════════════════════════════════════════════ */
function renderPagination(pagination, totalPages, total) {
if (!totalPages || totalPages <= 1) return;
pagination.innerHTML = `
<button class="kd-page-btn" id="kd-prev" ${currentPage === 1 ? "disabled" : ""}>◀</button>
${Array.from({ length: totalPages }, (_, i) => i + 1)
.map(
(p) => `
<button class="kd-page-btn ${p === currentPage ? "kd-page-active" : ""}" data-page="${p}">${p}</button>
`,
)
.join("")}
<button class="kd-page-btn" id="kd-next" ${currentPage === totalPages ? "disabled" : ""}>▶</button>
<span class="kd-page-info">${total} Karten</span>`;
pagination.querySelector("#kd-prev")?.addEventListener("click", async () => {
if (currentPage > 1) {
currentPage--;
await loadUserCards();
}
});
pagination.querySelector("#kd-next")?.addEventListener("click", async () => {
if (currentPage < totalPages) {
currentPage++;
await loadUserCards();
}
});
pagination.querySelectorAll("[data-page]").forEach((btn) => {
btn.addEventListener("click", async () => {
currentPage = parseInt(btn.dataset.page);
await loadUserCards();
});
});
}
/* ══════════════════════════════════════════════
EVENTS (Shell-Ebene)
══════════════════════════════════════════════ */
function attachShellEvents() {
// Deck-Auswahl direkt auf Element, kein document-Listener
const sel = document.getElementById("kd-deck-select");
if (sel) {
sel.addEventListener("change", async (e) => {
const val = parseInt(e.target.value);
if (!val) {
currentDeckId = null;
deckCards = [];
document.getElementById("kd-deck-grid").innerHTML =
`<div class="kd-empty-deck">Kein Deck ausgewählt.</div>`;
document.getElementById("kd-deck-info").innerHTML = "";
renderCollectionGrid(
document.getElementById("kd-grid"),
userCardsCache,
);
return;
}
currentDeckId = val;
await loadDeckCards(currentDeckId);
});
}
// Neues Deck direkt auf Button, kein document-Listener
const btnNew = document.getElementById("kd-btn-new-deck");
if (btnNew) {
btnNew.addEventListener("click", () => showNewDeckModal());
}
}
/* ══════════════════════════════════════════════
MODAL: Neues Deck
══════════════════════════════════════════════ */
function showNewDeckModal() {
if (decks.length >= 10) {
showToast("Du hast bereits 10 Decks (Maximum erreicht).");
return;
}
const wrap = document.querySelector(".kd-split");
const modal = document.createElement("div");
modal.className = "kd-modal-overlay";
modal.innerHTML = `
<div class="kd-modal">
<h3>Neues Deck erstellen</h3>
<input type="text" id="kd-new-deck-name" maxlength="100" placeholder="Deck-Name..." autofocus>
<div class="kd-modal-btns">
<button class="kd-modal-btn" id="kd-modal-cancel">Abbrechen</button>
<button class="kd-modal-btn primary" id="kd-modal-confirm">Erstellen</button>
</div>
</div>`;
wrap.appendChild(modal);
const input = modal.querySelector("#kd-new-deck-name");
input.focus();
modal.querySelector("#kd-modal-cancel").onclick = () => modal.remove();
modal.querySelector("#kd-modal-confirm").onclick = async () => {
const name = input.value.trim();
if (!name) {
input.focus();
return;
}
modal.remove();
await createDeck(name);
};
input.addEventListener("keydown", async (e) => {
if (e.key === "Enter") {
const name = input.value.trim();
if (!name) return;
modal.remove();
await createDeck(name);
}
if (e.key === "Escape") modal.remove();
});
}
/* ══════════════════════════════════════════════
HELPERS
══════════════════════════════════════════════ */
function getDeckCount(cardId) {
const entry = deckCards.find((c) => c.card_id === cardId);
return entry ? entry.amount : 0;
}
function isMaxedOut(card, inDeck, totalInDeck) {
if (!currentDeckId) return false;
if (totalInDeck >= 30) return true;
if (inDeck >= card.amount) return true; // mehr als besessen
if (parseInt(card.rarity) > 5 && inDeck >= 1) return true; // Rarity > 5: nur 1x
return false;
}
function showToast(msg) {
const existing = document.querySelector(".kd-toast");
if (existing) existing.remove();
const t = document.createElement("div");
t.className = "kd-toast";
t.textContent = msg;
t.style.cssText = `
position:fixed; bottom:80px; left:50%; transform:translateX(-50%);
background:linear-gradient(#4a2808,#2a1004); border:2px solid #8b6a3c;
border-radius:8px; color:#f0d9a6; font-family:"Cinzel",serif; font-size:13px;
padding:10px 22px; z-index:9999; pointer-events:none;
animation:kd-pulse 0.3s ease;`;
document.body.appendChild(t);
setTimeout(() => t.remove(), 2800);
}