728 lines
31 KiB
HTML
728 lines
31 KiB
HTML
<!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, don’t 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; don’t 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; don’t 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; don’t 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>
|