Files
SmartCoin/SmartCoin/frontend/app.js
T
2026-05-12 03:17:27 +07:00

342 lines
12 KiB
JavaScript

let config = null;
let activeOrderId = null;
let timerHandle = null;
let selectedCurrency = "MMK";
const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => Array.from(document.querySelectorAll(selector));
async function api(path, options = {}) {
const response = await fetch(path, options);
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "Request failed");
return payload;
}
function money(amount) {
if (selectedCurrency === "USD") return `$${(Number(amount) / 3500).toFixed(2)}`;
return `${Number(amount).toLocaleString()} MMK`;
}
function showModal(title, body) {
if (!$("#modalBackdrop")) return alert(title);
$("#modalTitle").textContent = title;
$("#modalBody").innerHTML = body;
$("#modalBackdrop").hidden = false;
}
function closeModal() {
if ($("#modalBackdrop")) $("#modalBackdrop").hidden = true;
}
function setStatus(status) {
if (!$("#statusBadge")) return;
$("#statusBadge").textContent = status;
$("#statusBadge").className = `badge ${status}`;
}
function renderProducts() {
if (!config || !$("#gameSelect") || !$("#productSelect")) return;
const game = $("#gameSelect").value;
const products = config.menu[game] || {};
$("#productSelect").innerHTML = Object.entries(products)
.map(([name, price]) => `<option value="${name}">${name} - ${money(price)}</option>`)
.join("");
renderProductPreview();
if ($("#selectedGameTitle")) $("#selectedGameTitle").textContent = game || "Choose Product";
}
function renderProductPreview() {
if (!config || !$("#productPreview") || !$("#gameSelect") || !$("#productSelect")) return;
const game = $("#gameSelect").value;
const product = $("#productSelect").value;
const price = config.menu[game]?.[product];
$("#productPreview").textContent = price
? `${product}: ${money(price)} before unique payment digits`
: "Select a product to see price.";
}
function renderTrack(order, target) {
if (!target) return;
const completeText =
order.status === "completed"
? `<div class="notice">Your order has been completed successfully.</div>`
: "";
target.innerHTML = `
<div class="order-card">
<header>
<strong>${order.order_id}</strong>
<span class="badge ${order.status}">${order.status}</span>
</header>
<div class="order-meta">
<span>Game: ${order.game}</span>
<span>User ID: ${order.user_game_id}</span>
<span>Product: ${order.product}</span>
<span>Amount: ${money(order.amount)}</span>
<span>Payment: ${order.payment_method || "-"}</span>
<span>Created: ${order.created_at}</span>
</div>
${completeText}
</div>
`;
}
async function refreshOrder(orderId) {
if (!orderId) return;
const { order } = await api(`/api/orders/${encodeURIComponent(orderId)}`);
setStatus(order.status);
renderTrack(order, $("#trackResult"));
}
function startTimer(expiresAt) {
if (!$("#timer")) return;
clearInterval(timerHandle);
const tick = () => {
const remaining = Math.max(0, expiresAt - Math.floor(Date.now() / 1000));
const mins = String(Math.floor(remaining / 60)).padStart(2, "0");
const secs = String(remaining % 60).padStart(2, "0");
$("#timer").textContent = `${mins}:${secs}`;
if (remaining === 0) {
clearInterval(timerHandle);
refreshOrder(activeOrderId);
}
};
tick();
timerHandle = setInterval(tick, 1000);
}
function renderAdminOrder(order, token) {
const screenshot = order.screenshot
? `<a href="/${order.screenshot}" target="_blank" rel="noreferrer">View screenshot</a>`
: `<span>No screenshot</span>`;
return `
<div class="order-card">
<header>
<strong>${order.order_id}</strong>
<span class="badge ${order.status}">${order.status}</span>
</header>
<div class="order-meta">
<span>${order.game}</span>
<span>${order.user_game_id}</span>
<span>${order.product}</span>
<span>${money(order.amount)}</span>
<span>${order.payment_method || "-"}</span>
${screenshot}
</div>
<div class="actions">
<button data-admin-action="confirm" data-order-id="${order.order_id}" data-token="${token}">Confirm</button>
<button class="danger" data-admin-action="reject" data-order-id="${order.order_id}" data-token="${token}">Reject</button>
<button class="secondary" data-admin-action="complete" data-order-id="${order.order_id}" data-token="${token}">Completed</button>
</div>
</div>
`;
}
function renderPricingPage() {
if (!config || !$("#pricingGrid")) return;
$("#pricingGrid").innerHTML = Object.entries(config.menu)
.map(([game, products]) => {
const rows = Object.entries(products)
.map(([product, price]) => `
<div class="price-row">
<span>${product}</span>
<strong>${money(price)}</strong>
</div>
`)
.join("");
return `
<section class="panel price-panel">
<h2>${game}</h2>
<div class="price-list">${rows}</div>
<a class="price-action" href="/orders.html?game=${encodeURIComponent(game)}">Top Up ${game}</a>
</section>
`;
})
.join("");
}
function setupSharedControls() {
$("#currencyButton")?.addEventListener("click", () => {
const menu = $("#currencyMenu");
if (!menu) return;
menu.hidden = !menu.hidden;
$("#currencyButton").setAttribute("aria-expanded", String(!menu.hidden));
});
$$("[data-currency]").forEach((button) => {
button.addEventListener("click", () => {
selectedCurrency = button.dataset.currency;
if ($("#currencyButton")) $("#currencyButton").children[1].textContent = selectedCurrency;
if ($("#currencyMenu")) $("#currencyMenu").hidden = true;
renderProductPreview();
renderPricingPage();
if (activeOrderId) refreshOrder(activeOrderId);
});
});
$("#loginButton")?.addEventListener("click", () => {
showModal("Login / Register", "<p>Account login is not required for checkout yet. Customers can place an order directly and track it with the Order ID.</p>");
});
$$("[data-modal-link]").forEach((link) => {
link.addEventListener("click", (event) => {
event.preventDefault();
const content = {
terms: ["Terms of Service", "<p>Orders are processed after manual wallet verification. Wrong game IDs, wrong products, or mismatched payments may be rejected.</p>"],
privacy: ["Privacy Policy", "<p>Only order details, payment method, uploaded screenshot, and transaction notes are stored for verification and support.</p>"],
refund: ["Refund Policy", "<p>Rejected or unpaid orders are not processed. Refund requests must include the Order ID and wallet transaction proof.</p>"],
}[link.dataset.modalLink];
if (content) showModal(content[0], content[1]);
});
});
$$("[data-social]").forEach((button) => {
button.addEventListener("click", () => {
showModal(button.dataset.social, `<p>${button.dataset.social} link is not configured yet. Add your real page/link when ready.</p>`);
});
});
$("#modalClose")?.addEventListener("click", closeModal);
$("#modalBackdrop")?.addEventListener("click", (event) => {
if (event.target.id === "modalBackdrop") closeModal();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") closeModal();
});
}
function setupHomePage() {
$$("[data-game-link]").forEach((card) => {
card.addEventListener("click", (event) => {
event.preventDefault();
window.location.href = `/orders.html?game=${encodeURIComponent(card.dataset.gameLink)}`;
});
});
}
function setupOrdersPage() {
if (!$("#orderForm")) return;
$("#gameSelect").innerHTML = Object.keys(config.menu)
.map((game) => `<option value="${game}">${game}</option>`)
.join("");
const gameParam = new URLSearchParams(window.location.search).get("game");
if (gameParam && config.menu[gameParam]) $("#gameSelect").value = gameParam;
renderProducts();
$("#buyButton").disabled = false;
$("#buyButton").textContent = "Buy";
$("#gameSelect").addEventListener("change", renderProducts);
$("#productSelect").addEventListener("change", renderProductPreview);
if ($("#kbzpay")) $("#kbzpay").textContent = config.kbzpay_number;
if ($("#ayapay")) $("#ayapay").textContent = config.ayapay_number;
if ($("#accountName")) $("#accountName").textContent = config.account_name;
if ($("#ayaQrImage") && config.ayapay_qr_path) {
$("#ayaQrImage").src = config.ayapay_qr_path;
$("#ayaQrPanel").hidden = false;
$("#ayaQrImage").addEventListener("error", () => {
$("#ayaQrPanel").hidden = true;
});
}
$("#orderForm").addEventListener("submit", async (event) => {
event.preventDefault();
const payload = Object.fromEntries(new FormData(event.currentTarget).entries());
try {
const order = await api("/api/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
activeOrderId = order.order_id;
$("#paymentPanel").hidden = false;
$("#orderId").textContent = order.order_id;
$("#amount").textContent = money(order.amount);
setStatus("pending");
startTimer(Math.floor(Date.now() / 1000) + config.expiry_seconds);
$("#paymentPanel").scrollIntoView({ behavior: "smooth", block: "start" });
} catch (error) {
alert(error.message);
}
});
$("#paymentForm")?.addEventListener("submit", async (event) => {
event.preventDefault();
if (!activeOrderId) return;
try {
const { order } = await api(`/api/orders/${encodeURIComponent(activeOrderId)}/payment`, {
method: "POST",
body: new FormData(event.currentTarget),
});
setStatus(order.status);
showModal("Payment Uploaded", "<p>Screenshot uploaded. Please wait for manual wallet verification.</p>");
} catch (error) {
alert(error.message);
}
});
}
function setupTrackPage() {
$("#trackForm")?.addEventListener("submit", async (event) => {
event.preventDefault();
const orderId = new FormData(event.currentTarget).get("track_order_id");
try {
const { order } = await api(`/api/orders/${encodeURIComponent(orderId)}`);
renderTrack(order, $("#trackResult"));
} catch (error) {
$("#trackResult").innerHTML = `<div class="notice">${error.message}</div>`;
}
});
}
function setupAdmin() {
$("#adminForm")?.addEventListener("submit", async (event) => {
event.preventDefault();
const token = new FormData(event.currentTarget).get("token");
try {
const { orders } = await api(`/api/admin/orders?token=${encodeURIComponent(token)}`);
$("#adminOrders").innerHTML = orders.length
? orders.map((order) => renderAdminOrder(order, token)).join("")
: `<div class="notice">No orders yet.</div>`;
} catch (error) {
$("#adminOrders").innerHTML = `<div class="notice">${error.message}</div>`;
}
});
$("#adminOrders")?.addEventListener("click", async (event) => {
const button = event.target.closest("[data-admin-action]");
if (!button) return;
const { adminAction, orderId, token } = button.dataset;
try {
await api(`/api/admin/orders/${orderId}/${adminAction}?token=${encodeURIComponent(token)}`, {
method: "POST",
});
$("#adminForm").requestSubmit ? $("#adminForm").requestSubmit() : $("#adminForm").dispatchEvent(new Event("submit", { cancelable: true, bubbles: true }));
} catch (error) {
alert(error.message);
}
});
}
async function init() {
setupSharedControls();
setupHomePage();
try {
config = await api("/api/config");
setupOrdersPage();
setupTrackPage();
setupAdmin();
renderPricingPage();
} catch (error) {
if ($("#buyButton")) {
$("#buyButton").disabled = true;
$("#buyButton").textContent = "Menu unavailable";
}
showModal("Menu unavailable", `<p>${error.message}</p>`);
}
}
init();