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}]
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 = `
`;
return img.repeat(count);
}
/* ══════════════════════════════════════════════
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 `
`;
}
/* ══════════════════════════════════════════════
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("");
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 = `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;
}
// 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 =
`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);
const maxed = isMaxedOut(c, inDeck, totalInDeck);
return `

${c.attack != null ? `
${c.attack}` : ""}
${c.defends != null ? `
${c.defends}` : ""}
${c.cooldown != null ? `
${c.cooldown}` : ""}
${c.rarity ? `
${rarityImgs(c.rarity, 13)}
` : ""}
`;
})
.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 = `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);
const ownedAmt = ownedEntry ? ownedEntry.amount : "?";
return `

${c.attack != null ? `
${c.attack}` : ""}
${c.defends != null ? `
${c.defends}` : ""}
${c.cooldown != null ? `
${c.cooldown}` : ""}
${c.rarity ? `
${rarityImgs(c.rarity, 14)}
` : ""}
`;
})
.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 = `
${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 – 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 =
`Kein Deck ausgewählt.
`;
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 = `
`;
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);
}