Main_Website-Oblistudios/ASAshop.html

728 lines
31 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>
<div class="spacer"></div>
<nav class="links" aria-label="Primary">
<a href="https://www.oblistudios.com">Home</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> Oblistudios LLC</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>1 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>2 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>3 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>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>
// ========= CONFIG =========
const API = "https://pay.oblistudios.com";
// Map your product IDs -> Stripe price IDs (from your Stripe Dashboard)
// !!! REPLACE these placeholders with your real price IDs !!!
const PRICE_IDS = {
server1: "price_1SELDJDv9LGVOP85Tmvl29iG",
server2: "price_1SELEGDv9LGVOP85f22Gldvc",
server3: "price_1SELLTDv9LGVOP85tgk0E7a8",
slot30: "price_1SPrq0Dv9LGVOP85TWi8As7L",
mutants: "price_1SELMXDv9LGVOP85iEACTDNB",
starter: "price_1SELMzDv9LGVOP851QbLAPv9",
supporter1: "price_1SELNPDv9LGVOP85AbSjJjsq",
supporter2: "price_1SELNkDv9LGVOP85UrZHPivA",
supporter3: "price_1SELOCDv9LGVOP85eov30j3a",
supporter4: "price_1SELOhDv9LGVOP858hO64GGr",
supporter5: "price_1SELOzDv9LGVOP85P4xwgGI5",
supporter6: "price_1SELPJDv9LGVOP85nnG60pr8",
"map-small": "price_1SELPpDv9LGVOP854Dsizby3",
"map-medium": "price_1SELQDDv9LGVOP859sA0XmwU",
"map-large": "price_1SELQZDv9LGVOP85GNbTyOHH"
};
// No capacity limits — all servers unlimited
// Unlimited servers: no capacity accounting at all
const SERVER_CAPACITY = {};
const CAPACITY_IDS = new Set(); // no IDs are capacity-limited
const CAPACITY_TOTAL = Infinity;
function capacityUsed() { return 0; }
function canAddServers() { return true; }
// isServer will always be false with an empty set, which is fine
const isServer = id => CAPACITY_IDS.has(id);
// ===== products (unchanged from your file) =====
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: 2.50, 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 (trimmed & wired to your UI) =====
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); }
function cardHtml(p) {
const inCart = state.cart[p.id] || 0;
const capRow = `<div class="rem-row text-muted">Unlimited</div>`;
let infoBtn = '';
if (p.id === 'server1') infoBtn = `<button class="btn btn-secondary" data-info="server-class-1">Info</button>`;
if (p.id === 'server2') infoBtn = `<button class="btn btn-secondary" data-info="server-class-2">Info</button>`;
if (p.id === 'server3') 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>
${capRow}
<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));
}
if (state.sort === 'price-asc') list.sort((a, b) => a.price - b.price);
if (state.sort === 'price-desc') list.sort((a, b) => b.price - a.price);
if (state.sort === 'name') list.sort((a, b) => a.name.localeCompare(b.name));
grid.innerHTML = list.map(cardHtml).join('');
updateCartUi();
}
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>';
// refresh remaining labels/buttons
document.querySelectorAll('[data-rem-for]').forEach(el => {
const id = el.dataset.remFor;
if (isServer(id)) {
const left = Math.max(0, CAPACITY_TOTAL - capacityUsed());
el.textContent = left;
const card = el.closest('[data-product]');
const btn = card?.querySelector(`[data-add="${id}"]`);
if (btn) { btn.disabled = left <= 0; btn.textContent = left <= 0 ? 'Sold out' : 'Add to cart'; }
} else {
el.textContent = '∞';
}
});
}
// search/sort/category hooks
$('#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(); });
// add/inc/dec
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');
if (isServer(id) && !canAddServers(1))
return alert(`Sold out or limit reached. Remaining: ${Math.max(0, CAPACITY_TOTAL - capacityUsed())}`);
state.cart[id] = (state.cart[id] || 0) + 1; saveCart(); render();
}
if (inc) {
const id = inc.getAttribute('data-inc');
if (isServer(id) && !canAddServers(1))
return alert(`Sold out or limit reached. Remaining: ${Math.max(0, CAPACITY_TOTAL - capacityUsed())}`);
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]) delete state.cart[id];
saveCart(); render();
}
});
// 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'); });
// Info modals (use your existing markup ids)
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'); modals[k]?.classList.add('open'); modals[k]?.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'); m?.classList.remove('open'); m?.setAttribute('aria-hidden', 'true'); }
});
Object.values(modals).forEach(m => 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')); } });
// Placeholder canvases for missing images
function paintPlaceholders() {
document.querySelectorAll('canvas[data-id]').forEach(cv => {
const id = cv.dataset.id, 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 system-ui,Inter'; ctx.fillText(products.find(p => p.id === id)?.name || id, 12, cv.height - 12);
});
}
// Checkout → calls your existing /create-checkout-session with multiple line_items
async function checkout() {
const entries = Object.entries(state.cart || {});
if (!entries.length) return alert('Your cart is empty.');
// Build Stripe line_items from cart
const line_items = [];
for (const [id, qty] of entries) {
const price = PRICE_IDS[id];
if (!price) {
alert(`Missing Stripe price for: ${id}. Update PRICE_IDS in the page.`);
return;
}
if (qty > 0) line_items.push({ price, quantity: Number(qty) });
}
if (!line_items.length) return alert('Nothing to checkout.');
// Create session
const r = await fetch(`${API}/create-checkout-session`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
line_items,
success_url: location.origin + location.pathname + '?ok=1',
cancel_url: location.origin + location.pathname + '?cancel=1'
})
});
const j = await r.json().catch(() => null);
if (!r.ok || !j?.url) {
console.error('create-checkout-session failed', j || await r.text());
return alert(j?.error || 'Checkout failed.');
}
// Stripe hosted page
location.href = j.url;
}
document.getElementById('checkoutBtn').addEventListener('click', checkout);
// On successful return (?ok=1) clear cart
(function () {
const qp = new URLSearchParams(location.search);
if (qp.get('ok') === '1') {
try { localStorage.removeItem('obli.cart'); } catch { }
state.cart = {};
history.replaceState(null, '', location.pathname);
}
})();
// Boot
document.getElementById('year').textContent = new Date().getFullYear();
render();
paintPlaceholders();
</script>
</body>
</html>