// --- Utility functions --- function formatItemId(id) { if (!id && id !== 0) return "(unknown)"; id = String(id); // Remove any NBT/variant suffixes that might follow a space or bracket e.g. "minecraft:stone{...}" // Keep it simple: strip anything starting with '{' or '[' or ' ' after id body. id = id.split("{")[0].split("[")[0].trim(); // If there is a namespace, take the part after colon const parts = id.split(":"); let name = parts.length > 1 ? parts[1] : parts[0]; // Normalize common separators name = name.replace(/[-.]/g, "_"); // Replace multiple underscores with single space name = name.replace(/_+/g, " ").trim(); // If name contains uppercase letters (already pretty), keep spacing but fix underscores if (/[A-Z]/.test(name)) { // Replace underscores and keep casing; ensure single spaces name = name.replace(/_+/g, " ").trim(); return name; } // Lowercase everything then title-case just the first word for a natural look: // "sugar cane" -> "Sugar cane", "redstone_dust" -> "Redstone dust" name = name.toLowerCase().split(" ").filter(Boolean); if (name.length === 0) return id; // Title-case only first word, leave rest lowercase (matches examples like "Sugar cane") const first = name[0]; const rest = name.slice(1); const firstCap = first.charAt(0).toUpperCase() + first.slice(1); const result = [firstCap].concat(rest).join(" "); return result; } // /api/users output is {{"ok":true,"users":[{"username":"ZareMate","password":"74b669f042afd3fccff6d3915f436b5bf3ad98836650ecfacf5062c7229115cb","balance":12858,"adresses":["PKP Centralny: Poczta - ZareMate","Factory 1: Manual"]},{"username":"qawe","password":"cd9ecb80ec16c7d73f7713f5e25a5d96b68bfe4debcfefc214f5351ee325e09f","balance":7027.52,"adresses":["Jajomyje 17: qawe"]},{"username":"skybloczek1","password":"937824fc6a52074b0a173319ec94c5e6c5a82cf8e63d9f401b15219c7e33248a","balance":22,"adresses":["Duchnice: skypa"]}]}} async function getAllAddresses() { try { const response = await fetch("/api/users"); const data = await response.json(); const addresses = data.users.map((user) => user.adresses).flat(); console.log(addresses); return addresses; } catch (error) { console.error("Error fetching addresses:", error); throw error; } } // get id from name function getId(name) { // Implement id retrieval logic here // name: Sugar cane -> minecraft:sugar_cane // name: Redstone dust -> minecraft:redstone_dust const parts = name.split(" "); const first = parts[0].toLowerCase(); const rest = parts.slice(1).map((word) => word.toLowerCase()); const id = [first].concat(rest).join("_"); return `minecraft:${id}`; } function getImage(id) { // Implement image retrieval logic here // id: mod:item_id // /static/textures/mod/item_id.png const parts = id.split(":"); const mod = parts[0]; const item = parts[1]; return `/static/textures/${mod}/${item}.png`; } function sha256(str) { // Returns a promise that resolves to the hex digest const encoder = new TextEncoder(); const data = encoder.encode(str); return crypto.subtle.digest("SHA-256", data).then((buf) => Array.from(new Uint8Array(buf)) .map((b) => b.toString(16).padStart(2, "0")) .join(""), ); } function openTransferModal() { const LocalUser = getLoggedInUser(); if (!LocalUser) { document.getElementById("transferMsg").textContent = "Please log in to transfer balance."; document.getElementById("transferModal").style.display = "flex"; return; } const transferToList = document.getElementById("transferToList"); transferToList.innerHTML = ""; Promise.all([fetch("/api/users").then((res) => res.json())]).then( ([users]) => { users.users.forEach((user) => { if (user.username !== LocalUser.username) { const input = document.createElement("input"); input.type = "radio"; input.name = "transferTo"; input.id = user.username; const label = document.createElement("label"); label.htmlFor = user.username; label.textContent = user.username; transferToList.appendChild(input); transferToList.appendChild(label); transferToList.appendChild(document.createElement("br")); } }); }, ); document.getElementById("transferMsg").textContent = ""; document.getElementById("transferModal").style.display = "flex"; } function closeTransferModal() { document.getElementById("transferModal").style.display = "none"; } // on onclick transferForm make POST request to /api/users/transfer function submitTransferForm() { const from = getLoggedInUser().username; const to = document.querySelector("input[name='transferTo']:checked").id; const amount = document.getElementById("transferAmount").value; fetch("/api/users/transfer", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ from, to, amount }), }) .then((res) => res.json()) .then((data) => { if (data.ok) { updateBalance(); closeTransferModal(); } else { document.getElementById("transferMsg").textContent = data.error; } }); } function updateBalance() { const user = getLoggedInUser(); if (!user) { document.getElementById("balanceAmount").textContent = 0; document.getElementById("balanceBtn").style.display = "none"; return; } fetch("/api/users/" + encodeURIComponent(user.username)) .then((res) => res.json()) .then((data) => { if (data.ok) { document.getElementById("balanceAmount").textContent = data.user.balance + " $"; document.getElementById("balanceBtn").style.display = "block"; } else { document.getElementById("balanceAmount").textContent = 0; document.getElementById("balanceBtn").style.display = "none"; } }); } document.getElementById("balanceBtn").addEventListener("click", () => { openTransferModal(); }); document.getElementById("transferBtn").addEventListener("click", () => { submitTransferForm(); }); // Helper to fetch user info (fresh) async function fetchUserInfo(username) { const res = await fetch("/api/users/" + encodeURIComponent(username)); const data = await res.json(); return data.ok ? data.user : null; } function showMsg(msg, error = false) { const msgEl = document.getElementById("msg"); msgEl.textContent = msg; msgEl.className = error ? "error" : ""; // after 2 seconds clear message setTimeout(() => { msgEl.textContent = ""; msgEl.className = ""; }, 2000); } // --- Login logic --- function openLoginModal() { document.getElementById("loginModal").style.display = "flex"; document.getElementById("loginMsg").textContent = ""; } function closeLoginModal() { document.getElementById("loginModal").style.display = "none"; } function updateLoginUI() { const user = getLoggedInUser(); document.getElementById("loginBtn").style.display = user ? "none" : ""; document.getElementById("logoutBtn").style.display = user ? "" : "none"; document.getElementById("accountBtn").style.display = user ? "" : "none"; updateBalance(); } function cartPosition() { const user = getLoggedInUser(); if (user) { document.querySelector(".cart-btn").style.right = "315px"; } else { document.querySelector(".cart-btn").style.right = "120px"; } } function getLoggedInUser() { try { return JSON.parse(localStorage.getItem("user")); } catch { return null; } } function setLoggedInUser(user) { localStorage.setItem("user", JSON.stringify(user)); updateLoginUI(); cartPosition(); updateBalance(); } function logout() { localStorage.removeItem("user"); updateLoginUI(); cartPosition(); updateBalance(); showMsg("Logged out."); } // --- Account Modal logic --- document.getElementById("accountBtn").onclick = openAccountModal; function openAccountModal() { showAccountTab("Password"); loadAddressesTab(); loadComputersTab(); document.getElementById("accountModal").style.display = "flex"; } function closeAccountModal() { document.getElementById("accountModal").style.display = "none"; } function showAccountTab(tab) { ["Password", "Addresses", "Computers"].forEach((t) => { document.getElementById("accountTab" + t).style.display = t === tab ? "" : "none"; document.getElementById("tab" + t).style.background = t === tab ? "#23272a" : ""; }); } document.getElementById("tabPassword").onclick = () => showAccountTab("Password"); document.getElementById("tabAddresses").onclick = () => { showAccountTab("Addresses"); loadAddressesTab(); }; document.getElementById("tabComputers").onclick = () => { showAccountTab("Computers"); loadComputersTab(); }; // --- Change Password --- document.getElementById("changePasswordForm").onsubmit = async function (e) { e.preventDefault(); const user = getLoggedInUser(); const msgEl = document.getElementById("changePasswordMsg"); msgEl.textContent = ""; if (!user) { msgEl.textContent = "Not logged in."; return; } const current = document.getElementById("currentPassword").value; const newpw = document.getElementById("newPassword").value; const confirm = document.getElementById("confirmNewPassword").value; if (!current || !newpw || !confirm) { msgEl.textContent = "Fill all fields."; return; } if (newpw !== confirm) { msgEl.textContent = "Passwords do not match."; return; } const userInfo = await fetchUserInfo(user.username); if (!userInfo) { msgEl.textContent = "User not found."; return; } const currentHash = await sha256(current); if (userInfo.password !== currentHash) { msgEl.textContent = "Current password incorrect."; return; } const newHash = await sha256(newpw); const res = await fetch("/api/users/" + encodeURIComponent(user.username), { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password: newHash }), }); const data = await res.json(); if (data.ok) { msgEl.style.color = "#23a559"; msgEl.textContent = "Password changed!"; } else { msgEl.style.color = "#ff5555"; msgEl.textContent = "Error: " + (data.error || "Unknown error"); } }; // --- Addresses Tab --- async function loadAddressesTab() { const user = getLoggedInUser(); const listEl = document.getElementById("addressesList"); const msgEl = document.getElementById("addressesMsg"); listEl.innerHTML = ""; msgEl.textContent = ""; if (!user) { listEl.innerHTML = "Not logged in."; return; } const userInfo = await fetchUserInfo(user.username); if (!userInfo || !Array.isArray(userInfo.adresses)) { listEl.innerHTML = "No addresses found."; return; } if (!userInfo.adresses.length) { listEl.innerHTML = "
No addresses.
"; } else { userInfo.adresses.forEach((addr, idx) => { const div = document.createElement("div"); div.style.display = "flex"; div.style.alignItems = "center"; div.style.marginBottom = "6px"; div.innerHTML = `${addr} `; listEl.appendChild(div); }); } } window.removeAddress = async function (idx) { const user = getLoggedInUser(); const msgEl = document.getElementById("addressesMsg"); msgEl.textContent = ""; if (!user) return; const userInfo = await fetchUserInfo(user.username); if (!userInfo || !Array.isArray(userInfo.adresses)) return; userInfo.adresses.splice(idx, 1); const res = await fetch("/api/users/" + encodeURIComponent(user.username), { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ adresses: userInfo.adresses }), }); const data = await res.json(); if (data.ok) { msgEl.style.color = "#23a559"; msgEl.textContent = "Address removed."; loadAddressesTab(); } else { msgEl.style.color = "#ff5555"; msgEl.textContent = "Error: " + (data.error || "Unknown error"); } }; document.getElementById("addAddressForm").onsubmit = async function (e) { e.preventDefault(); const user = getLoggedInUser(); const msgEl = document.getElementById("addressesMsg"); msgEl.textContent = ""; const newAddr = document.getElementById("newAddress").value.trim(); if (!user || !newAddr) return; const userInfo = await fetchUserInfo(user.username); if (!userInfo || !Array.isArray(userInfo.adresses)) return; userInfo.adresses.push(newAddr); const res = await fetch("/api/users/" + encodeURIComponent(user.username), { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ adresses: userInfo.adresses, }), }); // update local storage localStorage.setItem("user", JSON.stringify(userInfo)); const data = await res.json(); if (data.ok) { msgEl.style.color = "#23a559"; msgEl.textContent = "Address added."; document.getElementById("newAddress").value = ""; loadAddressesTab(); } else { msgEl.style.color = "#ff5555"; msgEl.textContent = "Error: " + (data.error || "Unknown error"); } }; // --- Linked Computers Tab --- async function loadComputersTab() { const user = getLoggedInUser(); const listEl = document.getElementById("computersList"); const msgEl = document.getElementById("computersMsg"); listEl.innerHTML = ""; msgEl.textContent = ""; if (!user) { listEl.innerHTML = "Not logged in."; return; } const res = await fetch( "/api/users/" + encodeURIComponent(user.username) + "/computers", ); const data = await res.json(); if (!data.ok || !Array.isArray(data.computers)) { listEl.innerHTML = "No linked computers found."; return; } if (!data.computers.length) { listEl.innerHTML = "
No linked computers.
"; } else { data.computers.forEach((comp) => { const div = document.createElement("div"); div.style.display = "flex"; div.style.alignItems = "center"; div.style.marginBottom = "6px"; div.innerHTML = `Computer #${comp.id} `; listEl.appendChild(div); }); } } window.removeComputer = async function (id) { const user = getLoggedInUser(); const msgEl = document.getElementById("computersMsg"); msgEl.textContent = ""; if (!user) return; const res = await fetch("/api/computers/" + id, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ linked_user: null }), }); const data = await res.json(); if (data.ok) { msgEl.style.color = "#23a559"; msgEl.textContent = "Computer unlinked."; loadComputersTab(); } else { msgEl.style.color = "#ff5555"; msgEl.textContent = "Error: " + (data.error || "Unknown error"); } }; document.getElementById("loginBtn").onclick = openLoginModal; document.getElementById("logoutBtn").onclick = logout; document.getElementById("loginForm").onsubmit = async function (e) { e.preventDefault(); const username = document.getElementById("loginUsername").value.trim(); const password = document.getElementById("loginPassword").value; if (!username || !password) return; const hash = await sha256(password); // Fetch user info to verify login let res = await fetch("/api/users/" + encodeURIComponent(username)); let data = await res.json(); if (!data.ok || !data.user) { document.getElementById("loginMsg").textContent = "User not found"; return; } if (data.user.password !== hash) { document.getElementById("loginMsg").textContent = "Incorrect password"; return; } setLoggedInUser({ username: data.user.username, adresses: data.user.adresses, }); closeLoginModal(); updateBalance(); cartPosition(); }; // --- Cart logic --- function getCart() { try { return JSON.parse(localStorage.getItem("cart")) || []; } catch { return []; } } function setCart(cart) { localStorage.setItem("cart", JSON.stringify(cart)); updateCartUI(); } function addToCart(item, qty = 1) { let cart = getCart(); const idx = cart.findIndex((i) => i.id === item.id); if (idx >= 0) { cart[idx].qty += qty * item.quantity || 1; } else { cart.push({ ...item, qty: qty * item.quantity || 1 }); } setCart(cart); } function removeFromCart(id) { let cart = getCart().filter((i) => i.id !== id); setCart(cart); } function updateCartUI() { const cart = getCart(); const count = cart.reduce((a, i) => a + i.qty, 0); document.getElementById("cartCount").textContent = count; document.getElementById("cartCount").style.display = count ? "" : "none"; } function openCart() { renderCart(); document.getElementById("cartWindow").style.display = "flex"; } function closeCart() { document.getElementById("cartWindow").style.display = "none"; } document.getElementById("cartBtn").addEventListener("click", () => { openCart(); }); async function getItemSellQuantity(id) { const res = await fetch("/api/shop/items"); const data = await res.json(); if (!data.ok) throw new Error(data.error || "Failed to load"); const item = data.items.find((i) => i.id === id); return item ? item.quantity : 1; } function renderCart() { const cart = getCart(); const cartItems = document.getElementById("cartItems"); const cartTotal = document.getElementById("cartTotal"); if (!cart.length) { cartItems.innerHTML = "
Your cart is empty.
"; cartTotal.textContent = ""; document.getElementById("checkoutBtn").disabled = true; return; } cartItems.innerHTML = ""; let total = 0; Promise.all( cart.map(async (item) => { const sellQuantity = await getItemSellQuantity(item.id); const orderAmount = item.qty / sellQuantity; const price = item.price * orderAmount; const div = document.createElement("div"); div.style.display = "flex"; div.style.justifyContent = "space-between"; div.style.alignItems = "center"; div.style.marginBottom = "8px"; div.innerHTML = ` ${item.name} ${formatItemId(item.name)} × ${item.qty} ${price.toFixed(2)} $ `; cartItems.appendChild(div); return price; }), ).then((prices) => { total = prices.reduce((sum, p) => sum + p, 0); cartTotal.textContent = "Total: " + total.toFixed(2) + " $"; document.getElementById("checkoutBtn").disabled = false; }); } // --- Address selection and checkout --- function openAddressModal() { const user = getLoggedInUser(); if (!user || !Array.isArray(user.adresses) || !user.adresses.length) { document.getElementById("addressMsg").textContent = "No addresses found for your account."; document.getElementById("addressList").innerHTML = ""; document.getElementById("addressModal").style.display = "flex"; return; } document.getElementById("addressMsg").textContent = ""; const list = document.getElementById("addressList"); list.innerHTML = ""; user.adresses.forEach((addr, idx) => { const id = "addr_" + idx; const label = document.createElement("label"); label.innerHTML = ` ${addr}`; list.appendChild(label); }); // add separator for other user addresses const separator = document.createElement("hr"); separator.style.margin = "10px 0"; list.appendChild(separator); getAllAddresses().then((addresses) => { const otherAddr = addresses.filter((addr) => !user.adresses.includes(addr)); otherAddr.forEach((addr, idx) => { const id = "addr_" + idx; const label = document.createElement("label"); label.innerHTML = ` ${addr}`; list.appendChild(label); }); }); document.getElementById("addressModal").style.display = "flex"; } function closeAddressModal() { document.getElementById("addressModal").style.display = "none"; } document.getElementById("checkoutBtn").onclick = function () { const user = getLoggedInUser(); if (!user) { closeCart(); openLoginModal(); return; } openAddressModal(); }; document.getElementById("addressForm").onsubmit = function (e) { e.preventDefault(); const addr = document.querySelector('input[name="address"]:checked'); if (!addr) { document.getElementById("addressMsg").textContent = "Please select an address."; return; } // from client send post to API const user = getLoggedInUser(); const cart = getCart(); const items = cart.map((item) => ({ id: item.name, quantity: item.qty, sellQuantity: getItemSellQuantity(item.name), })); const request = { userId: user.username, addressId: addr.value, items: items, }; console.log(request); fetch("/api/shop/items/buy", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(request), }) .then((response) => response.json()) .then((data) => { // Conditional check if (data.ok === true) { console.log("Purchase successful!"); showMsg("Order placed for address: " + addr.value); updateBalance(); } else { console.log("Purchase failed or message did not match:", data); showMsg("Order failed"); } }) .catch((error) => { console.error("Error during fetch:", error); }); closeAddressModal(); closeCart(); setCart([]); updateCartUI(); }; //get Item stock async function getItemStock(itemId) { try { const res = await fetch(`/api/item/${itemId}`); const data = await res.json(); if (!data.ok) throw new Error(data.error || "Failed to load"); return data.item.count; } catch (error) { console.error("Error during fetch:", error); return null; } } function getCartItemCount(itemId) { const cart = getCart(); const item = cart.find((item) => item.id === itemId); return item ? item.count : 0; } //check if item can be added to cart (if item_stock > cart_item_count) async function canAddToCart(itemId) { const item_stock = (await getItemStock(itemId)) || 0; const cart_item_count = getCartItemCount(itemId); return item_stock > cart_item_count; } function convertStockToStackPlusRest(stock, quantity) { const stackSize = 64; const stacks = Math.floor(stock / stackSize); const rest = stock % stackSize; return { stacks, rest }; } // --- Shop items logic --- async function fetchItems() { const tableBody = document.querySelector("#items-list tbody"); if (!tableBody) return; // Clear table and show loader tableBody.innerHTML = "Loading items..."; try { const res = await fetch("/api/shop/items"); const data = await res.json(); if (!data.ok) throw new Error(data.error || "Failed to load"); if (!data.items.length) { tableBody.innerHTML = "No items available to buy."; return; } tableBody.innerHTML = ""; // Clear loader // sort items alphabetically const sortedItems = [...data.items].sort((a, b) => a.name.localeCompare(b.name), ); const rows = await Promise.all( sortedItems.map(async (item) => { const item_stock = (await getItemStock(item.name)) || 0; const row = document.createElement("tr"); row.innerHTML = ` ${item.name} ${formatItemId(item.name)} ${item.quantity} ${item.price.toFixed(2)} $ ${convertStockToStackPlusRest(item_stock, item.quantity).stacks ? `${convertStockToStackPlusRest(item_stock, item.quantity).stacks} stacks
` : ""}${convertStockToStackPlusRest(item_stock, item.quantity).rest ? `${convertStockToStackPlusRest(item_stock, item.quantity).rest} in stock` : ""} ${!convertStockToStackPlusRest(item_stock, item.quantity).stacks && !convertStockToStackPlusRest(item_stock, item.quantity).rest ? "Out of stock" : ""} `; return row; }), ); tableBody.innerHTML = ""; rows.forEach((row) => tableBody.appendChild(row)); function getItemSellQuantity(item) { const quantity = parseInt( document.getElementById(`quantity-input-${item.id}`).value, 10, ); return quantity; } // Add event listeners for buy buttons Array.from(tableBody.getElementsByClassName("buy-btn")).forEach((btn) => { btn.onclick = function () { const item = { id: parseInt(this.dataset.id, 10), name: this.dataset.name, price: parseFloat(this.dataset.price), quantity: parseInt(this.dataset.quantity, 10), }; addToCart(item, getItemSellQuantity(item)); updateCartUI(); }; }); } catch (err) { tableBody.innerHTML = "Error loading items."; } } // --- Initialization --- updateLoginUI(); updateCartUI(); fetchItems(); updateBalance(); cartPosition(); // Close modals on outside click window.onclick = function (event) { ["loginModal", "cartWindow", "addressModal"].forEach((id) => { const el = document.getElementById(id); if (event.target === el) el.style.display = "none"; }); };