This commit is contained in:
cay 2026-04-14 18:25:19 +01:00
parent 4b10b28588
commit b486199ca2
3 changed files with 147 additions and 31 deletions

View File

@ -476,3 +476,64 @@
align-items: center;
gap: 14px;
}
/* ── Mengenauswahl ───────────────────────── */
.baz-qty-wrap {
display: flex;
align-items: center;
gap: 6px;
}
.baz-qty-btn {
width: 28px;
height: 28px;
background: linear-gradient(#3a2810, #1a0f04);
border: 2px solid #6b4b2a;
border-radius: 6px;
color: #f0d9a6;
font-size: 16px;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: .15s;
line-height: 1;
}
.baz-qty-btn:hover {
border-color: #f0d060;
color: #fff;
}
.baz-qty-input {
width: 52px;
text-align: center;
background: rgba(0,0,0,0.5);
border: 2px solid #6b4b2a;
border-radius: 6px;
color: #f0d9a6;
font-family: "Cinzel", serif;
font-size: 14px;
font-weight: bold;
padding: 4px 0;
outline: none;
-moz-appearance: textfield;
}
.baz-qty-input::-webkit-inner-spin-button,
.baz-qty-input::-webkit-outer-spin-button { -webkit-appearance: none; }
.baz-qty-input:focus { border-color: #f0d060; }
.baz-qty-max {
padding: 4px 10px;
background: linear-gradient(#4a3010, #2a1808);
border: 2px solid #c8960c;
border-radius: 6px;
color: #f0d060;
font-family: "Cinzel", serif;
font-size: 10px;
font-weight: bold;
cursor: pointer;
letter-spacing: 1px;
transition: .15s;
}
.baz-qty-max:hover {
border-color: #f0d060;
color: #fff5cc;
}

View File

@ -179,18 +179,37 @@ async function loadShopCards() {
function showBuyConfirm(card) {
document.getElementById("baz-confirm-modal")?.remove();
const canAfford =
baz_wood >= card.price_wood &&
baz_iron >= card.price_iron &&
baz_gold >= card.price_gold &&
baz_gems >= card.price_gems;
const maxAffordable = Math.max(1, Math.min(99,
card.price_wood > 0 ? Math.floor(baz_wood / card.price_wood) : 99,
card.price_iron > 0 ? Math.floor(baz_iron / card.price_iron) : 99,
card.price_gold > 0 ? Math.floor(baz_gold / card.price_gold) : 99,
card.price_gems > 0 ? Math.floor(baz_gems / card.price_gems) : 99,
));
const priceStr = [
card.price_wood > 0 ? `🪵 ${card.price_wood} Holz` : "",
card.price_iron > 0 ? `⚙️ ${card.price_iron} Eisen` : "",
card.price_gold > 0 ? `🪙 ${card.price_gold} Gold` : "",
card.price_gems > 0 ? `💎 ${card.price_gems} Gems` : "",
].filter(Boolean).join(" ");
function calcTotal(qty) {
return {
wood: card.price_wood * qty,
iron: card.price_iron * qty,
gold: card.price_gold * qty,
gems: card.price_gems * qty,
};
}
function priceHtml(qty) {
const t = calcTotal(qty);
return [
t.wood > 0 ? `<span class="baz-price-wood">🪵 ${t.wood}</span>` : "",
t.iron > 0 ? `<span class="baz-price-iron">⚙️ ${t.iron}</span>` : "",
t.gold > 0 ? `<span class="baz-price-gold">🪙 ${t.gold}</span>` : "",
t.gems > 0 ? `<span class="baz-price-gems">💎 ${t.gems}</span>` : "",
].filter(Boolean).join(" ");
}
function canAffordQty(qty) {
const t = calcTotal(qty);
return baz_wood >= t.wood && baz_iron >= t.iron &&
baz_gold >= t.gold && baz_gems >= t.gems;
}
const modal = document.createElement("div");
modal.id = "baz-confirm-modal";
@ -202,33 +221,68 @@ function showBuyConfirm(card) {
onerror="this.src='/images/avatar_placeholder.svg'">
</div>
<div class="baz-confirm-name">${card.name}</div>
<div class="baz-confirm-price">${priceStr || "Kostenlos"}</div>
${!canAfford ? `<div class="baz-confirm-warn">⚠ Nicht genug Ressourcen</div>` : ""}
<div class="baz-qty-wrap">
<button class="baz-qty-btn" id="baz-qty-minus"></button>
<input class="baz-qty-input" id="baz-qty" type="number"
min="1" max="99" value="1">
<button class="baz-qty-btn" id="baz-qty-plus">+</button>
<button class="baz-qty-max" id="baz-qty-max">MAX</button>
</div>
<div class="baz-confirm-price" id="baz-total-price">${priceHtml(1) || "Kostenlos"}</div>
<div class="baz-confirm-warn" id="baz-afford-warn" style="display:none"> Nicht genug Ressourcen</div>
<div class="baz-confirm-btns">
<button class="baz-btn-cancel" id="baz-cancel">Abbrechen</button>
<button class="baz-btn-buy" id="baz-confirm" ${!canAfford?"disabled":""}>Kaufen</button>
<button class="baz-btn-buy" id="baz-confirm">Kaufen</button>
</div>
</div>`;
document.getElementById("bazaar-popup").appendChild(modal);
const qtyInput = modal.querySelector("#baz-qty");
const priceEl = modal.querySelector("#baz-total-price");
const warnEl = modal.querySelector("#baz-afford-warn");
const buyBtn = modal.querySelector("#baz-confirm");
function updateModal() {
const qty = Math.max(1, Math.min(99, parseInt(qtyInput.value) || 1));
qtyInput.value = qty;
priceEl.innerHTML = priceHtml(qty) || "Kostenlos";
const ok = canAffordQty(qty);
warnEl.style.display = ok ? "none" : "";
buyBtn.disabled = !ok;
}
qtyInput.addEventListener("input", updateModal);
modal.querySelector("#baz-qty-minus").onclick = () => {
qtyInput.value = Math.max(1, parseInt(qtyInput.value || 1) - 1); updateModal();
};
modal.querySelector("#baz-qty-plus").onclick = () => {
qtyInput.value = Math.min(99, parseInt(qtyInput.value || 1) + 1); updateModal();
};
modal.querySelector("#baz-qty-max").onclick = () => {
qtyInput.value = maxAffordable; updateModal();
};
updateModal();
modal.querySelector("#baz-cancel").onclick = () => modal.remove();
modal.querySelector(".baz-confirm-backdrop").onclick = () => modal.remove();
modal.querySelector("#baz-confirm").onclick = async () => {
const btn = modal.querySelector("#baz-confirm");
btn.disabled = true; btn.textContent = "…";
buyBtn.onclick = async () => {
const qty = Math.max(1, Math.min(99, parseInt(qtyInput.value) || 1));
buyBtn.disabled = true; buyBtn.textContent = "…";
try {
const res = await fetch("/api/bazaar/buy", {
method:"POST", headers:{"Content-Type":"application/json"},
body: JSON.stringify({ card_id: card.id }),
body: JSON.stringify({ card_id: card.id, quantity: qty }),
});
const data = await res.json();
if (!res.ok) { btn.textContent = data.error||"Fehler"; setTimeout(()=>modal.remove(),2000); return; }
if (!res.ok) { buyBtn.textContent = data.error||"Fehler"; setTimeout(()=>modal.remove(),2000); return; }
baz_wood=data.wood; baz_iron=data.iron; baz_gold=data.gold; baz_gems=data.gems;
updateCurrencyDisplay();
modal.remove();
showToast(`${card.name} gekauft!`);
showToast(`${qty}× ${card.name} gekauft!`);
await loadShopCards();
} catch { btn.textContent="Fehler"; setTimeout(()=>modal.remove(),2000); }
} catch { buyBtn.textContent="Fehler"; setTimeout(()=>modal.remove(),2000); }
};
}

View File

@ -76,8 +76,9 @@ router.get("/bazaar/cards", requireLogin, async (req, res) => {
*/
router.post("/bazaar/buy", requireLogin, async (req, res) => {
const userId = req.session.user.id;
const { card_id } = req.body;
const { card_id, quantity } = req.body;
if (!card_id) return res.status(400).json({ error: "card_id fehlt." });
const qty = Math.max(1, Math.min(99, parseInt(quantity) || 1));
try {
const [[card]] = await db.query(
@ -117,10 +118,10 @@ router.post("/bazaar/buy", requireLogin, async (req, res) => {
{ key: "gems", label: "Gems" },
];
for (const { key, label } of checks) {
const need = card[`price_${key}`];
const need = card[`price_${key}`] * qty;
if (need > 0 && (currency[key] || 0) < need) {
return res.status(400).json({
error: `Nicht genug ${label}. Benötigt: ${need}, Vorhanden: ${currency[key] || 0}`,
error: `Nicht genug ${label}. Benötigt: ${need} (${qty}×), Vorhanden: ${currency[key] || 0}`,
});
}
}
@ -133,18 +134,18 @@ router.post("/bazaar/buy", requireLogin, async (req, res) => {
gems = gems - ?
WHERE account_id = ?`,
[
card.price_wood,
card.price_iron,
card.price_gold,
card.price_gems,
card.price_wood * qty,
card.price_iron * qty,
card.price_gold * qty,
card.price_gems * qty,
userId,
],
);
await db.query(
`INSERT INTO user_cards (user_id, card_id, amount) VALUES (?, ?, 1)
ON DUPLICATE KEY UPDATE amount = amount + 1`,
[userId, card_id],
`INSERT INTO user_cards (user_id, card_id, amount) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE amount = amount + ?`,
[userId, card_id, qty, qty],
);
const [[updated]] = await db.query(