1036 lines
36 KiB
JavaScript
1036 lines
36 KiB
JavaScript
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()} ${c.range}</span>` : ""}
|
||
${c.race != null ? `<span class="kd-badge-race">${raceIcon()} ${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()} ${c.range}</span>` : ""}
|
||
${c.race != null ? `<span class="kd-badge-race">${raceIcon()} ${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);
|
||
}
|