801 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// --- 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 = "<div>No addresses.</div>";
} 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 = `<span style="flex:1;">${addr}</span>
<button style="background:#ff5555;padding:2px 8px;border-radius:4px;border:none;color:#fff;cursor:pointer;" onclick="removeAddress(${idx})">Remove</button>`;
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 = "<div>No linked computers.</div>";
} else {
data.computers.forEach((comp) => {
const div = document.createElement("div");
div.style.display = "flex";
div.style.alignItems = "center";
div.style.marginBottom = "6px";
div.innerHTML = `<span style="flex:1;">Computer #${comp.id}</span>
<button style="background:#ff5555;padding:2px 8px;border-radius:4px;border:none;color:#fff;cursor:pointer;" onclick="removeComputer(${comp.id})">Remove</button>`;
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 = "<div>Your cart is empty.</div>";
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 = `
<img src="${getImage(item.name)}" alt="${item.name}"
style="width:50px;height:50px;margin-right:8px;image-rendering: pixelated;">
<span class="item-name">${formatItemId(item.name)} × ${item.qty}</span>
<span class="item-price">${price.toFixed(2)} $</span>
<button style="background:#ff5555;padding:2px 8px;border-radius:4px;border:none;color:#fff;cursor:pointer;"
onclick="removeFromCart(${item.id});renderCart();updateCartUI();">
Remove
</button>
`;
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 = `<input type="radio" name="address" value="${addr}" id="${id}" ${idx === 0 ? "checked" : ""}/> ${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 = `<input type="radio" name="address" value="${addr}" id="${id}"/> ${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: item.quantity,
}));
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 = "<tr><td colspan='6'>Loading items...</td></tr>";
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 =
"<tr><td colspan='6'>No items available to buy.</td></tr>";
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 = `
<td><img src="${getImage(item.name)}" alt="${item.name}" style="width:50px;height:50px;image-rendering: pixelated;"></td>
<td class="item-name">${formatItemId(item.name)}</td>
<td class="item-quantity">${item.quantity}</td>
<td class="item-price">${item.price.toFixed(2)} $</td>
<td class="item-stock">${convertStockToStackPlusRest(item_stock, item.quantity).stacks ? `${convertStockToStackPlusRest(item_stock, item.quantity).stacks} stacks </br> ` : ""}${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" : ""}
<td><input type="number" min="1" max="${Math.floor(item_stock / item.quantity)}" id="quantity-input-${item.id}" style="width:40px;" value="1"></td>
<td><button class="buy-btn" data-id="${item.id}" data-name="${item.name}" data-price="${item.price}" data-quantity="${item.quantity}">Get</button></td>
`;
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 =
"<tr><td colspan='6' style='color:#ff5555;'>Error loading items.</td></tr>";
}
}
// --- 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";
});
};