Main_Website-Oblistudios/ASAshop.html

745 lines
32 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>Obli Studios 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="/auth/discord" id="loginDiscord">Login with 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://flush-flush-slow-managed.trycloudflare.com';
/* ===== 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', payment: 'https://buy.stripe.com/test_123tee' },
{ id: 'server2', name: 'Server Class 2', price: 50.00, category: 'servers', tag: 'Server monthly', img: 'img/PrivateServerCLASS02.png', payment: 'https://buy.stripe.com/test_123mug' },
{ id: 'server3', name: 'Server Class 3', price: 75.00, category: 'servers', tag: 'Server monthly', img: 'img/PrivateServerCLASS03.png', payment: 'https://buy.stripe.com/test_123ost' },
{ id: 'slot30', name: 'ASA Server Slot x30 days', price: 5.00, category: 'perks', tag: 'Server Perk monthly', img: 'img/ServerSlotX30.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'mutants', name: 'Mutated Creatures', price: 1.50, category: 'bundles', tag: 'Dino Pack One Time Payment 10 non-breedable', img: 'img/MutatedCretures.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'starter', name: 'Starter Pack', price: 1.50, category: 'bundles', tag: 'Starter Pack One Time Payment', img: 'img/StarterPack.png', payment: 'https://buy.stripe.com/test_123perk' },
{ 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', payment: 'https://buy.stripe.com/test_123perk' },
{ 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', payment: 'https://buy.stripe.com/test_123perk' },
{ 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', payment: 'https://buy.stripe.com/test_123perk' },
{ 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', payment: 'https://buy.stripe.com/test_123perk' },
{ 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', payment: 'https://buy.stripe.com/test_123perk' },
{ 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', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'map-small', name: 'Small Map manipulation', price: 5.00, category: 'maps', tag: 'Small Map manipulation Pack One Time Payment', img: 'img/SmallMap.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'map-medium', name: 'Medium Map manipulation', price: 10.00, category: 'maps', tag: 'Medium Map manipulation Pack One Time Payment', img: 'img/MediumMap.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'map-large', name: 'Large Map manipulation', price: 15.00, category: 'maps', tag: 'Large Map manipulation Pack One Time Payment', img: 'img/LargeMap.png', payment: 'https://buy.stripe.com/test_123perk' },
];
/* ===== 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)
.filter(([id]) => isServer(id))
.reduce((a, [, q]) => a + q, 0);
}
function canAddServers(qtyToAdd) {
return serversInCart() + qtyToAdd <= remaining;
}
/* ===== Render ===== */
function cardHtml(p) {
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> <span class="chip" data-remaining>Remaining: <span class="remN">?</span>/12</span>`;
if (p.name === 'Server Class 2') infoBtn = `<button class="btn btn-secondary" data-info="server-class-2">Info</button> <span class="chip" data-remaining>Remaining: <span class="remN">?</span>/12</span>`;
if (p.name === 'Server Class 3') infoBtn = `<button class="btn btn-secondary" data-info="server-class-3">Info</button> <span class="chip" data-remaining>Remaining: <span class="remN">?</span>/12</span>`;
return `<article class="card">
<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>
<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('');
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();
}
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();
}
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();
}
});
/* ===== 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 ===== */
const GENERIC_CHECKOUT = 'https://buy.stripe.com/test_generic';
document.getElementById('checkoutBtn').addEventListener('click', async () => {
const entries = Object.entries(state.cart);
if (!entries.length) return alert('Your cart is empty.');
const items = entries.map(([id, qty]) => {
const p = products.find(p => p.id === id);
return { id, name: p?.name || id, qty, price: p?.price || 0 };
});
// 1) Try to reserve on the server
const reserve = await fetch(`${API}/api/reserve`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
credentials: 'include', body: JSON.stringify({ items })
})
if (!reserve?.ok) {
const rem = typeof reserve?.remaining === 'number' ? reserve.remaining : remaining;
alert(`Sold out. Remaining: ${rem}. Adjust your cart.`);
await refreshRemaining(); // sync the UI
return;
}
// 2) Create a Stripe Checkout session
const session = await fetch(`${API}/api/create-checkout`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
credentials: 'include', body: JSON.stringify({ items })
})
if (!session?.url) {
alert('Could not start checkout. Try again.'); return;
}
window.location.href = session.url;
});
/* ===== 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);
});
}
// client.js (in your existing <script>)
let remaining = 12;
async function refreshRemaining() {
try {
const r = await fetch(`${API}/api/inventory`, { credentials: 'include' })
document.querySelectorAll('[data-remaining] .remN').forEach(s => s.textContent = remaining);
// Disable all three server buttons if none left:
if (remaining <= 0) {
document.querySelectorAll('[data-add="server1"],[data-add="server2"],[data-add="server3"]').forEach(btn => {
btn.disabled = true; btn.textContent = 'Sold out';
});
}
} catch { }
}
refreshRemaining();
setInterval(refreshRemaining, 30_000); // stay “24/7” up-to-date
</script>
</body>
</html>