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 `
`;
}
/* ══════════════════════════════════════════════
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.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 `
`;
}).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 = `
`;
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);
}