Main_Website-Oblistudios/ASAshop.html

792 lines
34 KiB
HTML
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.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ASA Shop</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
[data-add="server1"]:disabled, [data-add="server2"]:disabled, [data-add="server3"]:disabled {
opacity: .6;
cursor: not-allowed
}
article.card:has([data-add="server1"]:disabled), article.card:has([data-add="server2"]:disabled), article.card:has([data-add="server3"]:disabled) {
filter: grayscale(0.2);
}
:root {
--bg: #0b1224;
--card: #121b34;
--stroke: #243053;
--accent: #4aa3ff;
--text: #e9f1ff;
--muted: #9bb3d9;
--chip: #1a2a50;
--btn: #1e3566;
--btn2: #244272;
}
* {
box-sizing: border-box
}
body {
margin: 0;
font: 14px/1.5 system-ui,Segoe UI,Inter,Arial;
background: var(--bg);
color: var(--text)
}
header, footer {
border-bottom: 1px solid var(--stroke);
background: #0c1530
}
header .wrap, main .wrap, footer .wrap {
max-width: 1100px;
margin: 0 auto;
padding: 14px 16px
}
header .row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap
}
h1 {
font-size: 20px;
margin: 0 8px 0 0
}
.spacer {
flex: 1
}
input, select {
background: #0c1733;
color: var(--text);
border: 1px solid var(--stroke);
border-radius: 8px;
padding: 8px;
outline: none
}
.cart-btn, .btn {
background: var(--btn);
color: var(--text);
border: 1px solid var(--stroke);
padding: 8px 12px;
border-radius: 10px;
text-decoration: none;
cursor: pointer
}
.btn-secondary {
background: var(--btn2)
}
.grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fill,minmax(240px,1fr));
padding: 16px
}
.card {
background: var(--card);
border: 1px solid var(--stroke);
border-radius: 16px;
overflow: hidden;
display: flex;
flex-direction: column
}
.card .footer {
background: #0d1b3a; /* dark blue footer */
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
margin-top: auto;
color: #fff;
}
.card .footer h3 {
margin: 0;
font-size: 16px;
color: #fff;
}
.card .footer .meta {
color: #cbd6f2;
font-size: 13px;
}
.card .footer .price {
font-weight: 700;
color: #fff;
}
.card .footer .chip {
background: #1e3566;
color: #fff;
padding: 3px 8px;
border-radius: 999px;
font-size: 12px;
display: inline-block;
}
.card .footer .actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.card .footer .btn {
background: #1e3566;
border: 1px solid #243053;
color: #fff;
}
.card .img {
background: #0c1630;
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
}
.card .img img {
width: 100%;
height: auto;
object-fit: contain;
display: block;
}
.card .body {
padding: 12px
}
.price {
font-weight: 700
}
.chip {
background: var(--chip);
color: #bcd0ff;
padding: 3px 8px;
border-radius: 999px;
display: inline-block;
margin: 6px 0
}
.meta {
color: var(--muted);
font-size: 12px
}
.toolbar {
display: flex;
gap: 8px;
flex-wrap: wrap;
padding: 0 16px 8px
}
/* Drawer */
#drawer {
position: fixed;
inset: 0 0 0 auto;
width: 380px;
background: var(--card);
border-left: 1px solid var(--stroke);
transform: translateX(100%);
transition: .25s;
z-index: 50;
display: flex;
flex-direction: column
}
#drawer.open {
transform: none
}
#drawer header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
border-bottom: 1px solid var(--stroke)
}
#drawer .body {
padding: 12px;
overflow: auto;
flex: 1
}
#drawer .row {
display: grid;
grid-template-columns: 1fr auto auto;
gap: 10px;
align-items: center;
padding: 8px 0;
border-bottom: 1px dashed var(--stroke)
}
#drawer .qty {
display: flex;
gap: 6px;
align-items: center
}
#drawer .qty button {
width: 28px;
height: 28px;
border-radius: 8px;
border: 1px solid var(--stroke);
background: var(--btn)
}
#exitCart {
background: #8b0c0c;
border-color: #5e0909;
color: #fff;
padding: 4px 8px;
border-radius: 8px;
font-size: 12px
}
.total {
padding: 12px;
border-top: 1px solid var(--stroke)
}
/* Modals */
.modal {
position: fixed;
inset: 0;
background: rgba(0,0,0,.5);
display: none;
align-items: center;
justify-content: center;
z-index: 60
}
.modal.open {
display: flex
}
.modal__dialog {
background: var(--card);
border: 1px solid var(--stroke);
border-radius: 16px;
max-width: 720px;
width: 92%;
overflow: hidden
}
.modal__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--stroke)
}
.modal__body {
padding: 16px
}
.modal__close {
background: #24365e;
color: #fff;
border: 1px solid var(--stroke);
border-radius: 10px;
padding: 6px 10px;
cursor: pointer
}
footer {
border-top: 1px solid var(--stroke)
}
.muted {
color: var(--muted)
}
nav.links {
display: flex;
gap: 16px;
}
nav.links a {
color: #e9f1ff;
text-decoration: none;
font-weight: 500;
}
nav.links a:hover {
color: #4aa3ff;
}
</style>
</head>
<body>
<header>
<div class="wrap">
<div class="row">
<h1>BESTWCOAST Shop</h1>
<a class="cart-btn" href="https://discord.gg/kQrAQSSrez" id="loginDiscord">Login to our Discord</a>
<span id="whoami" class="muted"></span>
<div class="spacer"></div>
<nav class="links" aria-label="Primary">
<a href="https://www.oblistudios.com">Home</a>
<a href="https://www.oblistudios.com/ASAservers.html">Servers</a>
</nav>
<a href="#" class="cart-btn" id="openCart">🛒 Cart <span id="cartCount">0</span></a>
</div>
<div class="toolbar">
<input id="search" placeholder="Search products…" />
<select id="category" title="Category">
<option value="all">All categories</option>
<option value="servers">Servers</option>
<option value="perks">Server Perks</option>
<option value="maps">Custom Maps</option>
<option value="bundles">Bundles</option>
<option value="vip">VIP / Subscriptions</option>
</select>
<select id="sort" title="Sort">
<option value="featured">Featured</option>
<option value="price-asc">Price ↑</option>
<option value="price-desc">Price ↓</option>
<option value="name">Name A→Z</option>
</select>
</div>
</div>
</header>
<main>
<div class="wrap">
<section id="grid" class="grid"></section>
</div>
</main>
<!-- Drawer (Cart) -->
<aside id="drawer" aria-hidden="true">
<header>
<strong>Your Cart</strong>
<button id="exitCart" title="Close cart">Exit</button>
</header>
<div class="body" id="cartItems"></div>
<div class="total">
<div style="display:flex; align-items:center; justify-content:space-between">
<div>Total</div>
<div id="cartTotal">$0.00</div>
</div>
<button class="btn" id="checkoutBtn" style="width:100%; margin-top:10px">Checkout</button>
</div>
</aside>
<footer>
<div class="wrap">
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap">
<div class="muted">© <span id="year"></span> Obli Studios</div>
<div class="muted">— Build anywhere, dont block obelisks. Be kind.</div>
</div>
</div>
</footer>
<!-- Info Modal: Server Class 1 -->
<div id="infoModal" class="modal" aria-hidden="true">
<div class="modal__dialog">
<header class="modal__head">
<h3 style="margin:0">🔒 Private Server Tier (Class 1)</h3>
<button class="modal__close" data-close="infoModal">×</button>
</header>
<div class="modal__body">
<p><strong>Exclusive to you and your tribe.</strong></p>
<p>🏝 <strong>Server Setup:</strong> Mirrors main cluster (PVP, breeding, rates). <strong>20 slots</strong>, password-secured.</p>
<p>⚙️ <strong>Tools & Perks:</strong> Creature Management Tool + <strong>10× Mutation Potions</strong> per tier.</p>
<p>🎁 <strong>Starting Bonus:</strong> <strong>100,000 points</strong> at the start of each wipe.</p>
<p><strong>Setup:</strong> Usually <strong>≤ 2 business days</strong>. Never wipes while subscribed.</p>
<p>🏗 <strong>Building:</strong> All standard rules; dont block obelisks; otherwise build anywhere.</p>
<p>📜 <strong>Ownership:</strong> Tribe-only access. No outsider storage/griefing. Maintain a base on at least one main server.</p>
<p>🌐 <strong>Clustering:</strong> Linked within <strong>1 hour</strong> of any wipe.</p>
<p>💬 <strong>Support:</strong> After purchase, open a Discord ticket for setup.</p>
</div>
</div>
</div>
<!-- Info Modal: Server Class 2 -->
<div id="infoModal2" class="modal" aria-hidden="true">
<div class="modal__dialog">
<header class="modal__head">
<h3 style="margin:0">🔒 Private Server Tier (Class 2)</h3>
<button class="modal__close" data-close="infoModal2">×</button>
</header>
<div class="modal__body">
<p><strong>Exclusive to you and your tribe.</strong></p>
<p>🏝 <strong>Server Setup:</strong> Mirrors main cluster (PVP, breeding, rates). <strong>30 slots</strong>, password-secured.</p>
<p>⚙️ <strong>Tools & Perks:</strong> Creature Management Tool + <strong>10× Mutation Potions</strong> per tier.</p>
<p>🎁 <strong>Starting Bonus:</strong> <strong>200,000 points</strong> at the start of each wipe.</p>
<p><strong>Setup:</strong> Usually <strong>≤ 2 business days</strong>. Never wipes while subscribed.</p>
<p>🏗 <strong>Building:</strong> All standard rules; dont block obelisks; otherwise build anywhere.</p>
<p>📜 <strong>Ownership:</strong> Tribe-only access. No outsider storage/griefing. Maintain a base on at least one main server.</p>
<p>🌐 <strong>Clustering:</strong> Linked within <strong>1 hour</strong> of any wipe.</p>
<p>💬 <strong>Support:</strong> After purchase, open a Discord ticket for setup.</p>
</div>
</div>
</div>
<!-- Info Modal: Server Class 3 -->
<div id="infoModal3" class="modal" aria-hidden="true">
<div class="modal__dialog">
<header class="modal__head">
<h3 style="margin:0">🔒 Private Server Tier (Class 3)</h3>
<button class="modal__close" data-close="infoModal3">×</button>
</header>
<div class="modal__body">
<p><strong>Exclusive to you and your tribe.</strong></p>
<p>🏝 <strong>Server Setup:</strong> Mirrors main cluster (PVP, breeding, rates). <strong>50 slots</strong>, password-secured.</p>
<p>⚙️ <strong>Tools & Perks:</strong> Creature Management Tool + <strong>10× Mutation Potions</strong> per tier.</p>
<p>🎁 <strong>Starting Bonus:</strong> <strong>300,000 points</strong> at the start of each wipe.</p>
<p><strong>Setup:</strong> Usually <strong>≤ 2 business days</strong>. Never wipes while subscribed.</p>
<p>🏗 <strong>Building:</strong> All standard rules; dont block obelisks; otherwise build anywhere.</p>
<p>📜 <strong>Ownership:</strong> Tribe-only access. No outsider storage/griefing. Maintain a base on at least one main server.</p>
<p>🌐 <strong>Clustering:</strong> Linked within <strong>1 hour</strong> of any wipe.</p>
<p>💬 <strong>Support:</strong> After purchase, open a Discord ticket for setup.</p>
</div>
</div>
</div>
<script>
const API = "https://affiliated-lets-automatic-oak.trycloudflare.com";
const CAPACITY_IDS = new Set(['server1', 'server2', 'server3']);
let remaining = 12;
/* ===== Login banner (whoami) ===== */
async function getWhoAmI() {
try {
const r = await fetch(`${API}/api/inventory`, { credentials: 'include' })
if (!r.ok) return null;
return await r.json();
} catch { return null; }
}
getWhoAmI().then(u => {
if (u) document.getElementById('whoami').textContent =
`Signed in as ${u.global_name || u.username}`;
});
/* ===== Products (unique IDs) ===== */
const products = [
{ id: 'server1', name: 'Server Class 1', price: 25.00, category: 'servers', tag: 'Server monthly', img: 'img/PrivateServerCLASS01.png'},
{ id: 'server2', name: 'Server Class 2', price: 50.00, category: 'servers', tag: 'Server monthly', img: 'img/PrivateServerCLASS02.png'},
{ id: 'server3', name: 'Server Class 3', price: 75.00, category: 'servers', tag: 'Server monthly', img: 'img/PrivateServerCLASS03.png' },
{ id: 'slot30', name: 'ASA Server Slot x30 days', price: 5.00, category: 'perks', tag: 'Server Perk monthly', img: 'img/ServerSlotX30.png' },
{ id: 'mutants', name: 'Mutated Creatures', price: 1.50, category: 'bundles', tag: 'Dino Pack One Time Payment 10 non-breedable', img: 'img/MutatedCretures.png'},
{ id: 'starter', name: 'Starter Pack', price: 1.50, category: 'bundles', tag: 'Starter Pack One Time Payment', img: 'img/StarterPack.png'},
{ id: 'supporter1', name: 'ASA Server Supporter Pack 1', price: 5.00, category: 'vip', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 50,000 Points every week🎁 50,000 Bonus Points at the start of each wipe 🏅 Exclusive VIP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP1.png' },
{ id: 'supporter2', name: 'ASA Server Supporter Pack 2', price: 10.00, category: 'vip', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 100,000 Points every week🎁 100,000 Bonus Points at the start of each wipe 🏅 Exclusive VIP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP2.png' },
{ id: 'supporter3', name: 'ASA Server Supporter Pack 3', price: 15.00, category: 'vip', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 150,000 Points every week🎁 150,000 Bonus Points at the start of each wipe 🏅 Exclusive VIP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP3.png'},
{ id: 'supporter4', name: 'ASA Server Supporter Pack 4', price: 20.00, category: 'vip', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 500,000 Points every week🎁 500,000 Bonus Points at the start of each wipe 🏅 Exclusive MVP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP4.png'},
{ id: 'supporter5', name: 'ASA Server Supporter Pack 5', price: 25.00, category: 'vip', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 1,000,000 Points every week🎁 1,000,000 Bonus Points at the start of each wipe 🏅 Exclusive MVP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP5.png'},
{ id: 'supporter6', name: 'ASA Server Supporter Pack 6', price: 35.00, category: 'vip', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 1,500,000 Points every week🎁 1,500,000 Bonus Points at the start of each wipe 🏅 Exclusive MVP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP6.png'},
{ id: 'map-small', name: 'Small Map manipulation', price: 5.00, category: 'maps', tag: 'Small Map manipulation Pack One Time Payment', img: 'img/SmallMap.png'},
{ id: 'map-medium', name: 'Medium Map manipulation', price: 10.00, category: 'maps', tag: 'Medium Map manipulation Pack One Time Payment', img: 'img/MediumMap.png' },
{ id: 'map-large', name: 'Large Map manipulation', price: 15.00, category: 'maps', tag: 'Large Map manipulation Pack One Time Payment', img: 'img/LargeMap.png' },
];
/* ===== State / helpers ===== */
const state = { q: '', cat: 'all', sort: 'featured', cart: loadCart() };
const $ = s => document.querySelector(s);
const fmt = n => `$${n.toFixed(2)}`;
function saveCart() { localStorage.setItem('obli.cart', JSON.stringify(state.cart)); }
function loadCart() { try { return JSON.parse(localStorage.getItem('obli.cart') || '{}'); } catch { return {}; } }
function cartCount() { return Object.values(state.cart).reduce((a, b) => a + b, 0); }
function cartTotal() { return Object.entries(state.cart).reduce((s, [id, q]) => { const p = products.find(p => p.id === id); return s + (p ? p.price * q : 0) }, 0); }
const isServer = id => id === 'server1' || id === 'server2' || id === 'server3';
function serversInCart() {
return Object.entries(state.cart || {})
.reduce((n, [id, qty]) => n + (CAPACITY_IDS.has(id) ? Number(qty || 0) : 0), 0);
}
function canAddServers(qtyToAdd) {
return serversInCart() + qtyToAdd <= remaining;
}
/* ===== Render ===== */
function cardHtml(p) {
const remHtml = CAPACITY_IDS.has(p.id)
? `<div class="rem-row">Remaining: <span class="remN" data-rem-for="${p.id}">12</span>/12</div>`
: `<div class="rem-row text-muted">Unlimited</div>`;
const inCart = state.cart[p.id] || 0;
let infoBtn = '';
if (p.name === 'Server Class 1') infoBtn = `<button class="btn btn-secondary" data-info="server-class-1">Info</button>`;
if (p.name === 'Server Class 2') infoBtn = `<button class="btn btn-secondary" data-info="server-class-2">Info</button>`;
if (p.name === 'Server Class 3') infoBtn = `<button class="btn btn-secondary" data-info="server-class-3">Info</button>`;
return `<article class="card" data-product="${p.id}">
<div class="img">${p.img ? `<img src="${p.img}" alt="${p.name}">` : `<canvas data-id="${p.id}" width="320" height="150"></canvas>`}</div>
<div class="footer">
<h3>${p.name}</h3>
<div class="meta">${p.tag || ''}</div>
<div class="price">${fmt(p.price)}</div>
<div class="chip">${p.category}</div>
${remHtml}
<div class="actions">
<button class="btn" data-add="${p.id}">${inCart ? 'Add another' : 'Add to cart'}</button>
${infoBtn}
</div>
</div>
</article>`;
}
function render() {
const grid = $('#grid');
let list = products.filter(p => state.cat === 'all' || p.category === state.cat);
if (state.q) {
const q = state.q.toLowerCase();
list = list.filter(p =>
p.name.toLowerCase().includes(q) ||
(p.tag || '').toLowerCase().includes(q) ||
(p.category || '').toLowerCase().includes(q)
);
}
switch (state.sort) {
case 'price-asc': list.sort((a, b) => a.price - b.price); break;
case 'price-desc': list.sort((a, b) => b.price - a.price); break;
case 'name': list.sort((a, b) => a.name.localeCompare(b.name)); break;
}
grid.innerHTML = list.map(cardHtml).join('');
refreshRemaining();
updateCartUi();
}
/* ===== Cart UI ===== */
function rowHtml(id, qty) {
const p = products.find(x => x.id === id);
return `<div class="row">
<div class="title"><div style="font-weight:600">${p.name}</div><div class="meta">${fmt(p.price)} each</div></div>
<div class="qty">
<button data-dec="${id}"></button>
<div>${qty}</div>
<button data-inc="${id}">+</button>
</div>
<div style="width:72px; text-align:right">${fmt(p.price * qty)}</div>
</div>`;
}
function updateCartUi() {
$('#cartCount').textContent = cartCount();
$('#cartTotal').textContent = fmt(cartTotal());
const rows = Object.entries(state.cart);
$('#cartItems').innerHTML = rows.length ? rows.map(([id, qty]) => rowHtml(id, qty)).join('') : '<div class="muted">Cart is empty.</div>';
}
/* ===== Events ===== */
$('#search').addEventListener('input', e => { state.q = e.target.value; render(); });
$('#category').addEventListener('change', e => { state.cat = e.target.value; render(); });
$('#sort').addEventListener('change', e => { state.sort = e.target.value; render(); });
document.body.addEventListener('click', e => {
const add = e.target.closest('[data-add]');
const inc = e.target.closest('[data-inc]');
const dec = e.target.closest('[data-dec]');
if (add) {
const id = add.getAttribute('data-add');
const qtyToAdd = 1;
if (isServer(id) && !canAddServers(qtyToAdd)) {
alert(`Sold out or limit reached. Remaining: ${Math.max(0, remaining - serversInCart())}.`);
return;
}
state.cart[id] = (state.cart[id] || 0) + 1;
saveCart(); render(); refreshRemaining();
}
if (inc) {
const id = inc.getAttribute('data-inc');
const qtyToAdd = 1;
if (isServer(id) && !canAddServers(qtyToAdd)) {
alert(`Sold out or limit reached. Remaining: ${Math.max(0, remaining - serversInCart())}.`);
return;
}
state.cart[id] = (state.cart[id] || 0) + 1;
saveCart(); updateCartUi(); refreshRemaining();
}
if (dec) {
const id = dec.getAttribute('data-dec');
state.cart[id] = Math.max(0, (state.cart[id] || 0) - 1);
if (state.cart[id] === 0) delete state.cart[id];
saveCart(); render(); refreshRemaining();
}
});
/* ===== Modals (single global handlers) ===== */
const modals = {
'server-class-1': document.getElementById('infoModal'),
'server-class-2': document.getElementById('infoModal2'),
'server-class-3': document.getElementById('infoModal3'),
};
document.body.addEventListener('click', (e) => {
const infoBtn = e.target.closest('[data-info]');
if (infoBtn) {
const k = infoBtn.getAttribute('data-info');
const m = modals[k];
if (m) { m.classList.add('open'); m.setAttribute('aria-hidden', 'false'); }
}
const closeBtn = e.target.closest('.modal__close');
if (closeBtn) {
const id = closeBtn.getAttribute('data-close');
const m = id ? document.getElementById(id) : closeBtn.closest('.modal');
if (m) { m.classList.remove('open'); m.setAttribute('aria-hidden', 'true'); }
}
});
Object.values(modals).forEach(m => {
if (!m) return;
m.addEventListener('click', (e) => { if (e.target === m) { m.classList.remove('open'); m.setAttribute('aria-hidden', 'true'); } });
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { Object.values(modals).forEach(m => m?.classList.remove('open')); }
});
/* ===== Drawer ===== */
const drawer = document.getElementById('drawer');
document.getElementById('openCart').addEventListener('click', e => {
e.preventDefault(); drawer.classList.add('open'); drawer.setAttribute('aria-hidden', 'false'); updateCartUi();
});
document.getElementById('exitCart').addEventListener('click', () => {
drawer.classList.remove('open'); drawer.setAttribute('aria-hidden', 'true');
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') { drawer.classList.remove('open'); drawer.setAttribute('aria-hidden', 'true'); }
});
/* ===== Checkout ===== */
async function checkout(cart) {
// 1) reserve
const r1 = await fetch(`${API}/api/reserve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: cart })
});
const j1 = await r1.json();
if (!j1.ok) { alert(j1.error || 'reserve failed'); return; }
// 2) create checkout
const r2 = await fetch(`${API}/api/create-checkout`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
resKey: j1.resKey,
items: [],
success_url: location.origin + location.pathname + '?ok=1',
cancel_url: location.origin + location.pathname + '?cancel=1'
})
});
const j2 = await r2.json();
if (!j2.url) { alert(j2.error || 'create-checkout failed'); return; }
location.href = j2.url; // redirect to Stripe
}
// === Checkout warning ===
document.getElementById('checkoutBtn').addEventListener('click', (e) => {
e.preventDefault();
alert("⚠️ IMPORTANT: After checkout, you must contact a team member to receive your order.\n\nPlease open a ticket in Discord once payment is complete.");
});
document.getElementById('checkoutBtn').addEventListener('click', async () => {
const entries = Object.entries(state.cart || {});
if (!entries.length) return alert('Your cart is empty.');
alert("⚠️ IMPORTANT: After checkout, you must contact a team member to receive your order.\n\nPlease open a ticket in Discord once payment is complete.");
const items = entries.map(([id, qty]) => {
const p = products.find(p => p.id === id);
return { id, name: p?.name || id, qty: Number(qty || 1), price: p?.price || 0 };
});
await checkout(items);
});
/* ===== Boot ===== */
document.getElementById('year').textContent = new Date().getFullYear();
render();
paintPlaceholders();
/* ===== Placeholder art for items without images ===== */
function paintPlaceholders() {
document.querySelectorAll('canvas[data-id]').forEach(cv => {
const id = cv.getAttribute('data-id');
const ctx = cv.getContext('2d');
const g = ctx.createLinearGradient(0, 0, cv.width, cv.height);
g.addColorStop(0, '#0f1b3a'); g.addColorStop(1, '#1b2f5d');
ctx.fillStyle = g; ctx.fillRect(0, 0, cv.width, cv.height);
ctx.strokeStyle = 'rgba(255,255,255,.12)';
for (let i = 0; i < 7; i++) { ctx.beginPath(); ctx.moveTo(0, 22 * i); ctx.lineTo(cv.width, 22 * i); ctx.stroke(); }
ctx.fillStyle = '#aad0ff'; ctx.font = 'bold 16px Inter,system-ui';
ctx.fillText(products.find(p => p.id === id)?.name || id, 12, cv.height - 12);
});
}
async function refreshRemaining() {
try {
const r = await fetch(`${API}/api/inventory`);
if (!r.ok) return;
const j = await r.json();
if (typeof j.remaining === 'number') remaining = j.remaining;
// For each card, set remaining or ∞ and enable/disable add button
document.querySelectorAll('[data-rem-for]').forEach(el => {
const id = el.dataset.remFor;
const card = el.closest('[data-product]');
const addBtn = card?.querySelector(`[data-add="${id}"]`);
if (CAPACITY_IDS.has(id)) {
const left = Math.max(0, remaining - serversInCart());
el.textContent = left; // just the current number
if (addBtn) {
addBtn.disabled = left <= 0;
addBtn.textContent = left <= 0 ? 'Sold out' : 'Add to cart';
}
} else {
el.textContent = '∞';
if (addBtn) {
addBtn.disabled = false;
addBtn.textContent = 'Add to cart';
}
}
});
} catch { /* ignore */ }
}
</script>
</body>
</html>