diff --git a/app.js b/app.js
index 1bd3471..8071216 100644
--- a/app.js
+++ b/app.js
@@ -22,6 +22,7 @@ const equip = require("./routes/equip");
const equipment = require("./routes/equipment");
const blackmarket = require("./routes/blackmarket");
const mineRoute = require("./routes/mine");
+const carddeckRoutes = require("./routes/carddeck");
const arenaRoutes = require("./routes/arena");
const { registerArenaHandlers } = require("./sockets/arena");
const { registerChatHandlers } = require("./sockets/chat");
@@ -297,44 +298,9 @@ app.get("/api/buildings", requireLogin, async (req, res) => {
}
});
-/* ========================
- Card Groups API
-======================== */
-
-app.get("/api/card-groups", requireLogin, async (req, res) => {
- try {
- const [groups] = await db.query("SELECT * FROM card_groups ORDER BY id");
- res.json(groups);
- } catch (err) {
- console.error(err);
- res.status(500).json({ error: "DB Fehler" });
}
});
-/* ========================
- Cards API
-======================== */
-
-app.get("/api/cards", requireLogin, async (req, res) => {
- const { group_id, page = 1, limit = 12 } = req.query;
- const offset = (page - 1) * limit;
-
- try {
- const [cards] = await db.query(
- `SELECT c.*, cg.name AS group_name, cg.color AS group_color,
- cl.attack, cl.defense, cl.cooldown
- FROM cards c
- LEFT JOIN card_groups cg ON cg.id = c.group_id
- LEFT JOIN card_levels cl ON cl.card_id = c.id AND cl.level = 1
- WHERE c.group_id = ?
- LIMIT ? OFFSET ?`,
- [group_id, parseInt(limit), parseInt(offset)]
- );
- const [[{ total }]] = await db.query(
- "SELECT COUNT(*) as total FROM cards WHERE group_id = ?", [group_id]
- );
-
- res.json({ cards, total, page: parseInt(page), totalPages: Math.ceil(total / limit) });
} catch (err) {
console.error(err);
res.status(500).json({ error: "DB Fehler" });
@@ -367,6 +333,7 @@ app.use("/api/equip", equip);
app.use("/api/equipment", equipment);
app.use("/api/blackmarket", blackmarket);
app.use("/api/mine", mineRoute);
+app.use("/api", carddeckRoutes);
app.use("/arena", arenaRoutes);
/* ========================
diff --git a/public/js/quickmenu/carddeck.js b/public/js/quickmenu/carddeck.js
index 4601c39..0ca318d 100644
--- a/public/js/quickmenu/carddeck.js
+++ b/public/js/quickmenu/carddeck.js
@@ -1,254 +1,458 @@
-const CARDS_PER_PAGE = 12;
+const CARDS_PER_PAGE = 18;
-let currentGroupId = null;
-let currentPage = 1;
+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 loadCards();
+ await Promise.all([loadUserCards(), loadDecks()]);
}
} catch (err) {
- console.error("Gruppen Fehler:", err);
+ console.error("Init Fehler:", err);
}
}
-/* ── Grundstruktur ──────────────────────────── */
+/* ══════════════════════════════════════════════
+ SHELL HTML
+══════════════════════════════════════════════ */
function renderShell() {
return `
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Kein Deck ausgewählt.
+
+
+
+
`;
}
-/* ── Tabs aus DB ────────────────────────────── */
+/* ══════════════════════════════════════════════
+ TABS
+══════════════════════════════════════════════ */
function renderTabs(groups) {
const container = document.getElementById("kd-tabs");
if (!container) return;
@@ -261,8 +465,7 @@ function renderTabs(groups) {
style="border-color:${color};${i === 0 ? `background:${color}33;` : ""}">
${g.name}
-
- `;
+ `;
}).join("");
container.querySelectorAll(".kd-tab").forEach((btn) => {
@@ -275,14 +478,19 @@ function renderTabs(groups) {
btn.classList.add("kd-tab-active");
btn.style.background = `${color}33`;
currentGroupId = parseInt(btn.dataset.group);
- currentPage = 1;
- await loadCards();
+ currentPage = 1;
+ await loadUserCards();
});
});
}
-/* ── Karten laden ───────────────────────────── */
-async function loadCards() {
+/* ══════════════════════════════════════════════
+ 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;
@@ -291,54 +499,371 @@ async function loadCards() {
pagination.innerHTML = "";
try {
- const res = await fetch(`/api/cards?group_id=${currentGroupId}&page=${currentPage}&limit=${CARDS_PER_PAGE}`);
+ 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();
- renderGrid(grid, data.cards);
+ userCardsCache = data.cards || [];
+ renderCollectionGrid(grid, userCardsCache);
renderPagination(pagination, data.totalPages, data.total);
} catch {
- grid.innerHTML = `Noch keine Karten für diese Gruppe vorhanden.
`;
+ grid.innerHTML = `Keine Karten gefunden.
`;
}
}
-/* ── Grid ───────────────────────────────────── */
-function renderGrid(grid, cards) {
- if (!cards || !cards.length) {
- grid.innerHTML = `Noch keine Karten für diese Gruppe vorhanden.
`;
+/* ══════════════════════════════════════════════
+ 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;
}
- grid.innerHTML = cards.map((c) => `
-
-

- ${c.attack != null ? `
${c.attack}` : ""}
- ${c.defense != null ? `
${c.defense}` : ""}
-
${c.name}
-
- `).join("");
+
+ 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.");
+ }
}
-/* ── Pagination ─────────────────────────────── */
-function renderPagination(pagination, totalPages, total) {
- if (totalPages <= 1) return;
- pagination.innerHTML = `
-
- ${Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => `
-
- `).join("")}
-
- ${total} Karten
- `;
- document.getElementById("kd-prev")?.addEventListener("click", async () => {
- if (currentPage > 1) { currentPage--; await loadCards(); }
- });
- document.getElementById("kd-next")?.addEventListener("click", async () => {
- if (currentPage < totalPages) { currentPage++; await loadCards(); }
- });
- pagination.querySelectorAll("[data-page]").forEach((btn) => {
- btn.addEventListener("click", async () => {
- currentPage = parseInt(btn.dataset.page);
- await loadCards();
+/* ══════════════════════════════════════════════
+ 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;
+ }
+ grid.innerHTML = cards.map(c => `
+
+ ${c.amount > 1 ? `
${c.amount}×` : ""}
+

+
+
`).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);
+}
diff --git a/routes/carddeck.route.js b/routes/carddeck.route.js
new file mode 100644
index 0000000..d4c907d
--- /dev/null
+++ b/routes/carddeck.route.js
@@ -0,0 +1,326 @@
+const express = require("express");
+const router = express.Router();
+const db = require("../database/database");
+
+/* ========================
+ Auth Middleware
+======================== */
+
+function requireLogin(req, res, next) {
+ if (!req.session.user) return res.status(401).json({ error: "Nicht eingeloggt" });
+ next();
+}
+
+router.use(requireLogin);
+
+/* ════════════════════════════════════════════
+ GET /api/card-groups
+ Alle Kartengruppen
+════════════════════════════════════════════ */
+
+router.get("/card-groups", async (req, res) => {
+ try {
+ const [groups] = await db.query("SELECT * FROM card_groups ORDER BY id");
+ res.json(groups);
+ } catch (err) {
+ console.error(err);
+ res.status(500).json({ error: "DB Fehler" });
+ }
+});
+
+/* ════════════════════════════════════════════
+ GET /api/cards?group_id=X&page=Y&limit=Z
+ Alle Karten (Admin-/Gruppen-Ansicht, kein Besitz)
+════════════════════════════════════════════ */
+
+router.get("/cards", async (req, res) => {
+ const { group_id, page = 1, limit = 12 } = req.query;
+ const offset = (page - 1) * limit;
+
+ try {
+ const [cards] = await db.query(
+ `SELECT c.*, cg.name AS group_name, cg.color AS group_color,
+ cl.attack, cl.defense, cl.cooldown
+ FROM cards c
+ LEFT JOIN card_groups cg ON cg.id = c.group_id
+ LEFT JOIN card_levels cl ON cl.card_id = c.id AND cl.level = 1
+ WHERE c.group_id = ?
+ LIMIT ? OFFSET ?`,
+ [group_id, parseInt(limit), parseInt(offset)]
+ );
+ const [[{ total }]] = await db.query(
+ "SELECT COUNT(*) AS total FROM cards WHERE group_id = ?",
+ [group_id]
+ );
+ res.json({ cards, total, page: parseInt(page), totalPages: Math.ceil(total / limit) });
+ } catch (err) {
+ console.error(err);
+ res.status(500).json({ error: "DB Fehler" });
+ }
+});
+
+/* ════════════════════════════════════════════
+ GET /api/user-cards?group_id=X&page=Y&limit=Z
+ Karten die der Spieler besitzt (mit amount)
+════════════════════════════════════════════ */
+
+router.get("/user-cards", async (req, res) => {
+ const userId = req.session.user.id;
+ const { group_id, page = 1, limit = 18 } = req.query;
+ const offset = (page - 1) * limit;
+
+ try {
+ const [cards] = await db.query(
+ `SELECT
+ uc.card_id,
+ uc.level,
+ uc.amount,
+ c.name,
+ c.image,
+ cg.name AS group_name,
+ cg.color AS group_color,
+ cl.attack,
+ cl.defense,
+ cl.cooldown
+ FROM user_cards uc
+ JOIN cards c ON c.id = uc.card_id
+ JOIN card_groups cg ON cg.id = c.group_id
+ LEFT JOIN card_levels cl ON cl.card_id = uc.card_id AND cl.level = uc.level
+ WHERE uc.user_id = ? AND c.group_id = ?
+ ORDER BY c.id, uc.level
+ LIMIT ? OFFSET ?`,
+ [userId, group_id, parseInt(limit), parseInt(offset)]
+ );
+
+ const [[{ total }]] = await db.query(
+ `SELECT COUNT(*) AS total
+ FROM user_cards uc
+ JOIN cards c ON c.id = uc.card_id
+ WHERE uc.user_id = ? AND c.group_id = ?`,
+ [userId, group_id]
+ );
+
+ res.json({ cards, total, page: parseInt(page), totalPages: Math.ceil(total / limit) });
+ } catch (err) {
+ console.error(err);
+ res.status(500).json({ error: "DB Fehler" });
+ }
+});
+
+/* ════════════════════════════════════════════
+ GET /api/decks
+ Alle Decks des Spielers
+════════════════════════════════════════════ */
+
+router.get("/decks", async (req, res) => {
+ const userId = req.session.user.id;
+ try {
+ const [decks] = await db.query(
+ `SELECT d.id, d.name,
+ COALESCE(SUM(dc.amount), 0) AS card_count
+ FROM decks d
+ LEFT JOIN deck_cards dc ON dc.deck_id = d.id
+ WHERE d.user_id = ?
+ GROUP BY d.id
+ ORDER BY d.created_at`,
+ [userId]
+ );
+ res.json(decks);
+ } catch (err) {
+ console.error(err);
+ res.status(500).json({ error: "DB Fehler" });
+ }
+});
+
+/* ════════════════════════════════════════════
+ POST /api/decks { name }
+ Neues Deck erstellen (max. 10 pro Spieler)
+════════════════════════════════════════════ */
+
+router.post("/decks", async (req, res) => {
+ const userId = req.session.user.id;
+ const { name } = req.body;
+
+ if (!name || !name.trim()) {
+ return res.status(400).json({ error: "Name darf nicht leer sein." });
+ }
+
+ try {
+ const [[{ count }]] = await db.query(
+ "SELECT COUNT(*) AS count FROM decks WHERE user_id = ?",
+ [userId]
+ );
+ if (count >= 10) {
+ return res.status(400).json({ error: "Maximale Anzahl von 10 Decks erreicht." });
+ }
+
+ const [result] = await db.query(
+ "INSERT INTO decks (user_id, name) VALUES (?, ?)",
+ [userId, name.trim()]
+ );
+ res.status(201).json({ id: result.insertId, name: name.trim(), card_count: 0 });
+ } catch (err) {
+ console.error(err);
+ res.status(500).json({ error: "DB Fehler" });
+ }
+});
+
+/* ════════════════════════════════════════════
+ GET /api/decks/:id/cards
+ Karten eines bestimmten Decks
+════════════════════════════════════════════ */
+
+router.get("/decks/:id/cards", async (req, res) => {
+ const userId = req.session.user.id;
+ const deckId = req.params.id;
+
+ try {
+ // Deck gehört dem Spieler?
+ const [[deck]] = await db.query(
+ "SELECT id FROM decks WHERE id = ? AND user_id = ?",
+ [deckId, userId]
+ );
+ if (!deck) return res.status(404).json({ error: "Deck nicht gefunden." });
+
+ const [cards] = await db.query(
+ `SELECT
+ dc.card_id,
+ dc.level,
+ dc.amount,
+ c.name,
+ c.image,
+ cl.attack,
+ cl.defense
+ FROM deck_cards dc
+ JOIN cards c ON c.id = dc.card_id
+ LEFT JOIN card_levels cl ON cl.card_id = dc.card_id AND cl.level = dc.level
+ WHERE dc.deck_id = ?
+ ORDER BY c.name, dc.level`,
+ [deckId]
+ );
+ res.json(cards);
+ } catch (err) {
+ console.error(err);
+ res.status(500).json({ error: "DB Fehler" });
+ }
+});
+
+/* ════════════════════════════════════════════
+ POST /api/decks/:id/cards { card_id, level }
+ Karte zum Deck hinzufügen
+════════════════════════════════════════════ */
+
+router.post("/decks/:id/cards", async (req, res) => {
+ const userId = req.session.user.id;
+ const deckId = req.params.id;
+ const { card_id, level = 1 } = req.body;
+
+ if (!card_id) return res.status(400).json({ error: "card_id fehlt." });
+
+ try {
+ // Deck gehört dem Spieler?
+ const [[deck]] = await db.query(
+ "SELECT id FROM decks WHERE id = ? AND user_id = ?",
+ [deckId, userId]
+ );
+ if (!deck) return res.status(404).json({ error: "Deck nicht gefunden." });
+
+ // Besitzt der Spieler diese Karte mit diesem Level?
+ const [[owned]] = await db.query(
+ "SELECT amount FROM user_cards WHERE user_id = ? AND card_id = ? AND level = ?",
+ [userId, card_id, level]
+ );
+ if (!owned) return res.status(400).json({ error: "Du besitzt diese Karte nicht." });
+
+ // Deck-Gesamtzahl prüfen (max 30)
+ const [[{ total }]] = await db.query(
+ "SELECT COALESCE(SUM(amount), 0) AS total FROM deck_cards WHERE deck_id = ?",
+ [deckId]
+ );
+ if (total >= 30) {
+ return res.status(400).json({ error: "Deck ist voll (max. 30 Karten)." });
+ }
+
+ // Level > 5: max. 1× im Deck
+ const [[existing]] = await db.query(
+ "SELECT amount FROM deck_cards WHERE deck_id = ? AND card_id = ? AND level = ?",
+ [deckId, card_id, level]
+ );
+
+ if (level > 5 && existing) {
+ return res.status(400).json({ error: "Karten ab Level 6 dürfen nur einmal im Deck sein." });
+ }
+
+ // Nicht mehr einfügen als besessen
+ const currentInDeck = existing ? existing.amount : 0;
+ if (currentInDeck >= owned.amount) {
+ return res.status(400).json({ error: "Du hast keine weiteren Exemplare dieser Karte." });
+ }
+
+ // Einfügen oder erhöhen
+ if (existing) {
+ await db.query(
+ "UPDATE deck_cards SET amount = amount + 1 WHERE deck_id = ? AND card_id = ? AND level = ?",
+ [deckId, card_id, level]
+ );
+ } else {
+ await db.query(
+ "INSERT INTO deck_cards (deck_id, card_id, level, amount) VALUES (?, ?, ?, 1)",
+ [deckId, card_id, level]
+ );
+ }
+
+ res.json({ success: true });
+ } catch (err) {
+ // DB-Trigger Fehler (45000) sauber weitergeben
+ if (err.sqlState === "45000") {
+ return res.status(400).json({ error: err.message });
+ }
+ console.error(err);
+ res.status(500).json({ error: "DB Fehler" });
+ }
+});
+
+/* ════════════════════════════════════════════
+ DELETE /api/decks/:id/cards { card_id, level }
+ Karte aus Deck entfernen (amount - 1, bei 0 löschen)
+════════════════════════════════════════════ */
+
+router.delete("/decks/:id/cards", async (req, res) => {
+ const userId = req.session.user.id;
+ const deckId = req.params.id;
+ const { card_id, level = 1 } = req.body;
+
+ if (!card_id) return res.status(400).json({ error: "card_id fehlt." });
+
+ try {
+ // Deck gehört dem Spieler?
+ const [[deck]] = await db.query(
+ "SELECT id FROM decks WHERE id = ? AND user_id = ?",
+ [deckId, userId]
+ );
+ if (!deck) return res.status(404).json({ error: "Deck nicht gefunden." });
+
+ const [[entry]] = await db.query(
+ "SELECT id, amount FROM deck_cards WHERE deck_id = ? AND card_id = ? AND level = ?",
+ [deckId, card_id, level]
+ );
+ if (!entry) return res.status(404).json({ error: "Karte nicht im Deck." });
+
+ if (entry.amount > 1) {
+ await db.query(
+ "UPDATE deck_cards SET amount = amount - 1 WHERE id = ?",
+ [entry.id]
+ );
+ } else {
+ await db.query("DELETE FROM deck_cards WHERE id = ?", [entry.id]);
+ }
+
+ res.json({ success: true });
+ } catch (err) {
+ console.error(err);
+ res.status(500).json({ error: "DB Fehler" });
+ }
+});
+
+module.exports = router;