Add files via upload

This commit is contained in:
James 2025-10-25 23:28:30 -07:00 committed by GitHub
parent 21b2ec1f76
commit ed0b1036b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 223 additions and 314 deletions

View File

@ -460,44 +460,34 @@
</div> </div>
</div> </div>
<script> <script>
// --- clear cart helper --- // ========= CONFIG =========
function clearCart() {
try { localStorage.removeItem('asa_cart'); } catch { }
if (window.state && state.cart) {
state.cart = {};
if (typeof renderCart === 'function') renderCart();
}
}
// --- clear on successful return from Stripe ---
(function () {
const qp = new URLSearchParams(location.search);
if (qp.get('ok') === '1') {
clearCart();
// optional: remove ?ok=1 from the URL bar
history.replaceState(null, '', location.pathname);
}
})();
</script>
<script>
const API = "https://pay.oblistudios.com"; 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_1SELM0Dv9LGVOP85vvEWN8XA",
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"
};
// Which SKUs count against the 12-unit capacity
const CAPACITY_IDS = new Set(['server1', 'server2', 'server3']); const CAPACITY_IDS = new Set(['server1', 'server2', 'server3']);
let remaining = 12; const CAPACITY_TOTAL = 12;
/* ===== Login banner (whoami) ===== */ // ===== products (unchanged from your file) =====
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 = [ const products = [
{ id: 'server1', name: 'Server Class 1', price: 25.00, category: 'servers', tag: 'Server monthly', img: 'img/PrivateServerCLASS01.png' }, { 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: 'server2', name: 'Server Class 2', price: 50.00, category: 'servers', tag: 'Server monthly', img: 'img/PrivateServerCLASS02.png' },
@ -519,7 +509,7 @@
{ id: 'map-large', name: 'Large Map manipulation', price: 15.00, category: 'maps', tag: 'Large Map manipulation Pack One Time Payment', img: 'img/LargeMap.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 ===== */ // ===== state/helpers (trimmed & wired to your UI) =====
const state = { q: '', cat: 'all', sort: 'featured', cart: loadCart() }; const state = { q: '', cat: 'all', sort: 'featured', cart: loadCart() };
const $ = s => document.querySelector(s); const $ = s => document.querySelector(s);
const fmt = n => `$${n.toFixed(2)}`; const fmt = n => `$${n.toFixed(2)}`;
@ -527,26 +517,20 @@
function loadCart() { try { return JSON.parse(localStorage.getItem('obli.cart') || '{}'); } catch { return {}; } } 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 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 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'; const isServer = id => CAPACITY_IDS.has(id);
function serversInCart() { const serversInCart = () => Object.entries(state.cart).reduce((n, [id, qty]) => n + (CAPACITY_IDS.has(id) ? Number(qty || 0) : 0), 0);
return Object.entries(state.cart || {}) const canAddServers = (qty) => serversInCart() + qty <= CAPACITY_TOTAL;
.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) { function cardHtml(p) {
const remHtml = CAPACITY_IDS.has(p.id) const inCart = state.cart[p.id] || 0;
? `<div class="rem-row">Remaining: <span class="remN" data-rem-for="${p.id}">12</span>/12</div>` const capRow = isServer(p.id)
? `<div class="rem-row">Remaining: <span class="remN" data-rem-for="${p.id}">${Math.max(0, CAPACITY_TOTAL - serversInCart())}</span>/${CAPACITY_TOTAL}</div>`
: `<div class="rem-row text-muted">Unlimited</div>`; : `<div class="rem-row text-muted">Unlimited</div>`;
const inCart = state.cart[p.id] || 0;
let infoBtn = ''; let infoBtn = '';
if (p.name === 'Server Class 1') infoBtn = `<button class="btn btn-secondary" data-info="server-class-1">Info</button>`; if (p.id === 'server1') 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.id === 'server2') 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>`; 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}"> 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="img">${p.img ? `<img src="${p.img}" alt="${p.name}">` : `<canvas data-id="${p.id}" width="320" height="150"></canvas>`}</div>
@ -555,40 +539,29 @@
<div class="meta">${p.tag || ''}</div> <div class="meta">${p.tag || ''}</div>
<div class="price">${fmt(p.price)}</div> <div class="price">${fmt(p.price)}</div>
<div class="chip">${p.category}</div> <div class="chip">${p.category}</div>
${remHtml} ${capRow}
<div class="actions"> <div class="actions">
<button class="btn" data-add="${p.id}">${inCart ? 'Add another' : 'Add to cart'}</button> <button class="btn" data-add="${p.id}" ${isServer(p.id) && !canAddServers(1) ? 'disabled' : ''}>${inCart ? 'Add another' : 'Add to cart'}</button>
${infoBtn} ${infoBtn}
</div> </div>
</div> </div>
</article>`; </article>`;
} }
function render() { function render() {
const grid = $('#grid'); const grid = $('#grid');
let list = products.filter(p => state.cat === 'all' || p.category === state.cat); let list = products.filter(p => state.cat === 'all' || p.category === state.cat);
if (state.q) { if (state.q) {
const q = state.q.toLowerCase(); const q = state.q.toLowerCase();
list = list.filter(p => list = list.filter(p => p.name.toLowerCase().includes(q) || (p.tag || '').toLowerCase().includes(q) || (p.category || '').toLowerCase().includes(q));
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;
} }
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(''); grid.innerHTML = list.map(cardHtml).join('');
refreshRemaining();
updateCartUi(); updateCartUi();
} }
/* ===== Cart UI ===== */
function rowHtml(id, qty) { function rowHtml(id, qty) {
const p = products.find(x => x.id === id); const p = products.find(x => x.id === id);
return `<div class="row"> return `<div class="row">
@ -606,55 +579,56 @@
$('#cartTotal').textContent = fmt(cartTotal()); $('#cartTotal').textContent = fmt(cartTotal());
const rows = Object.entries(state.cart); 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>'; $('#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 - serversInCart());
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 = '∞';
}
});
} }
/* ===== Events ===== */ // search/sort/category hooks
$('#search').addEventListener('input', e => { state.q = e.target.value; render(); }); $('#search').addEventListener('input', e => { state.q = e.target.value; render(); });
$('#category').addEventListener('change', e => { state.cat = e.target.value; render(); }); $('#category').addEventListener('change', e => { state.cat = e.target.value; render(); });
$('#sort').addEventListener('change', e => { state.sort = e.target.value; render(); }); $('#sort').addEventListener('change', e => { state.sort = e.target.value; render(); });
// add/inc/dec
document.body.addEventListener('click', e => { document.body.addEventListener('click', e => {
const add = e.target.closest('[data-add]'); const add = e.target.closest('[data-add]');
const inc = e.target.closest('[data-inc]'); const inc = e.target.closest('[data-inc]');
const dec = e.target.closest('[data-dec]'); const dec = e.target.closest('[data-dec]');
if (add) { if (add) {
const id = add.getAttribute('data-add'); const id = add.getAttribute('data-add');
const qtyToAdd = 1; if (isServer(id) && !canAddServers(1)) return alert(`Sold out or limit reached. Remaining: ${Math.max(0, CAPACITY_TOTAL - serversInCart())}`);
if (isServer(id) && !canAddServers(qtyToAdd)) { state.cart[id] = (state.cart[id] || 0) + 1; saveCart(); render();
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) { if (inc) {
const id = inc.getAttribute('data-inc'); const id = inc.getAttribute('data-inc');
const qtyToAdd = 1; if (isServer(id) && !canAddServers(1)) return alert(`Sold out or limit reached. Remaining: ${Math.max(0, CAPACITY_TOTAL - serversInCart())}`);
if (isServer(id) && !canAddServers(qtyToAdd)) { state.cart[id] = (state.cart[id] || 0) + 1; saveCart(); updateCartUi();
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) { if (dec) {
const id = dec.getAttribute('data-dec'); const id = dec.getAttribute('data-dec');
state.cart[id] = Math.max(0, (state.cart[id] || 0) - 1); state.cart[id] = Math.max(0, (state.cart[id] || 0) - 1);
if (state.cart[id] === 0) delete state.cart[id]; if (!state.cart[id]) delete state.cart[id];
saveCart(); render(); refreshRemaining(); 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'); });
/* ===== Modals (single global handlers) ===== */ // Info modals (use your existing markup ids)
const modals = { const modals = {
'server-class-1': document.getElementById('infoModal'), 'server-class-1': document.getElementById('infoModal'),
'server-class-2': document.getElementById('infoModal2'), 'server-class-2': document.getElementById('infoModal2'),
@ -662,142 +636,77 @@
}; };
document.body.addEventListener('click', (e) => { document.body.addEventListener('click', (e) => {
const infoBtn = e.target.closest('[data-info]'); const infoBtn = e.target.closest('[data-info]');
if (infoBtn) { if (infoBtn) { const k = infoBtn.getAttribute('data-info'); modals[k]?.classList.add('open'); modals[k]?.setAttribute('aria-hidden', 'false'); }
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'); const closeBtn = e.target.closest('.modal__close');
if (closeBtn) { 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'); }
const id = closeBtn.getAttribute('data-close'); });
const m = id ? document.getElementById(id) : closeBtn.closest('.modal'); Object.values(modals).forEach(m => m?.addEventListener('click', e => { if (e.target === m) { m.classList.remove('open'); m.setAttribute('aria-hidden', 'true'); } }));
if (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);
});
} }
});
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 ===== */ // Checkout → calls your existing /create-checkout-session with multiple line_items
const drawer = document.getElementById('drawer'); async function checkout() {
document.getElementById('openCart').addEventListener('click', e => { const entries = Object.entries(state.cart || {});
e.preventDefault(); drawer.classList.add('open'); drawer.setAttribute('aria-hidden', 'false'); updateCartUi(); if (!entries.length) return alert('Your cart is empty.');
});
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 ===== */ // 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
async function checkout(cart) { const r = await fetch(`${API}/create-checkout-session`, {
// 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
// 2) create checkout
const r2 = await fetch(`${API}/api/create-checkout`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
resKey: j1.resKey, line_items,
items: cart, // ← send your cart, not []
success_url: location.origin + location.pathname + '?ok=1', success_url: location.origin + location.pathname + '?ok=1',
cancel_url: location.origin + location.pathname + '?cancel=1' cancel_url: location.origin + location.pathname + '?cancel=1'
}) })
}); });
const j2 = await r2.json(); const j = await r.json().catch(() => null);
if (!r2.ok || !j2.url) { alert(j2.error || 'create-checkout failed'); return; } if (!r.ok || !j?.url) {
location.href = j2.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', async () => {
const entries = Object.entries(state.cart || {});
if (!entries.length) return alert('Your cart is empty.');
const items = entries.map(([id, qty]) => { document.getElementById('checkoutBtn').addEventListener('click', checkout);
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); // 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
/* ===== Boot ===== */
document.getElementById('year').textContent = new Date().getFullYear(); document.getElementById('year').textContent = new Date().getFullYear();
render(); render();
paintPlaceholders(); paintPlaceholders();
</script>
/* ===== 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> </body>
</html> </html>