342 lines
12 KiB
JavaScript
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();
|