const CARDS_PER_PAGE = 18; let currentGroupId = null; let currentPage = 1; let currentDeckId = null; let deckCards = []; // [{card_id, level, amount}] let userCardsCache = []; // aktuelle Seite: [{card_id, level, amount, name, image, attack, defense}] let decks = []; // [{id, name}] /* ══════════════════════════════════════════════ INIT ══════════════════════════════════════════════ */ export async function loadCardDeck() { const body = document.getElementById("qm-body-carddeck"); if (!body) return; 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 `
Meine Karten
Lade Karten...
Deck-Builder
Kein Deck ausgewählt.
`; } /* ══════════════════════════════════════════════ 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 ` `; }).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 = `
Lade Karten...
`; 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 = `
Keine Karten gefunden.
`; } } /* ══════════════════════════════════════════════ 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 = `` + decks.map(d => ``).join(""); } /* ══════════════════════════════════════════════ 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 = `
Lade Deck...
`; 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 = ` Karten im Deck: ${total} / 30`; renderDeckGrid(grid, deckCards); // Sammlung neu rendern damit Deck-Zähler aktualisiert werden renderCollectionGrid(document.getElementById("kd-grid"), userCardsCache); } catch { grid.innerHTML = `
Fehler beim Laden des Decks.
`; } } /* ══════════════════════════════════════════════ 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; } // Level > 5: max. 1 Exemplar im Deck if (card.level > 5) { const already = deckCards.find(c => c.card_id === card.card_id && c.level === card.level); if (already) { showToast("Karten ab Level 6 dürfen nur 1x im Deck sein."); return; } } // Nicht mehr als vorhanden besitzt const owned = card.amount; const inDeck = getDeckCount(card.card_id, card.level); 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, level: card.level }), }); 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, level: card.level }), }); 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(); currentDeckId = deck.id; renderDeckSelect(); deckCards = []; document.getElementById("kd-deck-grid").innerHTML = `
Deck ist leer.
Klicke links auf eine Karte um sie hinzuzufügen.
`; document.getElementById("kd-deck-info").innerHTML = `Karten im Deck:0 / 30`; 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 = `
Keine Karten in dieser Gruppe.
`; return; } const totalInDeck = deckCards.reduce((s, c) => s + c.amount, 0); grid.innerHTML = cards.map(c => { const inDeck = getDeckCount(c.card_id, c.level); const maxed = isMaxedOut(c, inDeck, totalInDeck); return `
${c.name}
${c.name} ${c.attack != null ? `${c.attack}` : ""} ${c.defense != null ? `${c.defense}` : ""}
`; }).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) && c.level === parseInt(el.dataset.level) ); if (card) await addCardToDeck(card); }); }); } /* ══════════════════════════════════════════════ RENDER: DECK-GRID ══════════════════════════════════════════════ */ function renderDeckGrid(grid, cards) { if (!cards || !cards.length) { grid.innerHTML = `
Deck ist leer.
Klicke links auf eine Karte um sie hinzuzufügen.
`; return; } // Owned-Anzahl aus userCardsCache auslesen grid.innerHTML = cards.map(c => { const ownedEntry = userCardsCache.find(u => u.card_id === c.card_id && u.level === c.level); const ownedAmt = ownedEntry ? ownedEntry.amount : "?"; return `
${c.name}
`; }).join(""); grid.querySelectorAll(".kd-deck-card").forEach(el => { el.addEventListener("click", async () => { const card = cards.find( c => c.card_id === parseInt(el.dataset.cardId) && c.level === parseInt(el.dataset.level) ); if (card) await removeCardFromDeck(card); }); }); } /* ══════════════════════════════════════════════ PAGINATION ══════════════════════════════════════════════ */ function renderPagination(pagination, totalPages, total) { if (!totalPages || totalPages <= 1) return; pagination.innerHTML = ` ${Array.from({ length: totalPages }, (_, i) => i + 1).map(p => ` `).join("")} ${total} Karten`; 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 document.addEventListener("change", async (e) => { if (e.target.id !== "kd-deck-select") return; const val = parseInt(e.target.value); if (!val) { currentDeckId = null; deckCards = []; document.getElementById("kd-deck-grid").innerHTML = `
Kein Deck ausgewählt.
`; document.getElementById("kd-deck-info").innerHTML = ""; renderCollectionGrid(document.getElementById("kd-grid"), userCardsCache); return; } currentDeckId = val; await loadDeckCards(currentDeckId); }); // Neues Deck document.addEventListener("click", (e) => { if (e.target.id !== "kd-btn-new-deck") return; 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 = `

Neues Deck erstellen

`; 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, level) { const entry = deckCards.find(c => c.card_id === cardId && c.level === level); 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 (card.level > 5 && inDeck >= 1) return true; // Level > 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); }