Add files via upload
This commit is contained in:
parent
b1a8bd7d6d
commit
4a49e7e18b
540
ASAshop.html
540
ASAshop.html
|
|
@ -338,10 +338,7 @@
|
|||
<div class="wrap">
|
||||
<div class="row">
|
||||
<h1>BESTWCOAST Shop</h1>
|
||||
<a class="cart-btn"
|
||||
href="#"
|
||||
id="loginDiscord"
|
||||
data-client-id="1423370765578797126">Login to our Discord</a>
|
||||
<a class="cart-btn" href="https://discord.gg/kQrAQSSrez" id="loginDiscord">Login to our Discord</a>
|
||||
<span id="whoami" class="muted"></span>
|
||||
<div class="spacer"></div>
|
||||
<nav class="links" aria-label="Primary">
|
||||
|
|
@ -375,64 +372,6 @@
|
|||
<section id="grid" class="grid"></section>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
const API = "https://affiliated-lets-automatic-oak.trycloudflare.com"; // unchanged
|
||||
|
||||
// ---- whoami banner (unchanged logic) ----
|
||||
async function getWhoAmI() {
|
||||
try {
|
||||
const r = await fetch(`${API}/api/inventory`, { credentials: 'include' });
|
||||
if (!r.ok) return null;
|
||||
const j = await r.json();
|
||||
const u = j?.user ?? j;
|
||||
const label = u?.global_name ?? u?.username ?? (u?.id ? `User ${u.id}` : null);
|
||||
return label ? { label, raw: u } : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
(async () => {
|
||||
const who = await getWhoAmI();
|
||||
document.getElementById('whoami').textContent =
|
||||
who ? `Signed in as ${who.label}` : 'Not signed in';
|
||||
})();
|
||||
|
||||
// ---- FIXED Discord login button ----
|
||||
document.getElementById('loginDiscord').addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const backTo = encodeURIComponent(location.href);
|
||||
const clientId = document.getElementById('loginDiscord').dataset.clientId;
|
||||
const backendStart = `${API}/api/auth/discord?redirect=${backTo}`;
|
||||
const callback = encodeURIComponent(`${API}/api/auth/discord/callback`);
|
||||
const direct = `https://discord.com/oauth2/authorize?client_id=${clientId}&redirect_uri=${callback}&response_type=code&scope=identify`;
|
||||
|
||||
try {
|
||||
// If backend is reachable, use the backend start route
|
||||
const ping = await fetch(`${API}/healthz`, { mode: 'no-cors' }).catch(() => null);
|
||||
// no-cors fetch won’t throw on 200, but if DNS/route fails we hit catch above
|
||||
location.href = backendStart;
|
||||
} catch {
|
||||
// Fallback: go straight to Discord’s authorize page
|
||||
location.href = direct;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
document.getElementById('checkoutBtn').addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const entries = Object.entries(state.cart || {});
|
||||
if (!entries.length) return alert('Your cart is empty.');
|
||||
|
||||
alert("⚠️ IMPORTANT: After checkout, you must contact a team member to receive your order.\n\nPlease open a ticket in Discord once payment is complete.");
|
||||
|
||||
const items = entries.map(([id, qty]) => {
|
||||
const p = products.find(p => p.id === id);
|
||||
return { id, name: p?.name || id, qty: Number(qty || 1), price: p?.price || 0 };
|
||||
});
|
||||
|
||||
await checkout(items);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Drawer (Cart) -->
|
||||
<aside id="drawer" aria-hidden="true">
|
||||
|
|
@ -523,90 +462,63 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
const API = "https://affiliated-lets-automatic-oak.trycloudflare.com";
|
||||
const API = "https://affiliated-lets-automatic-oak.trycloudflare.com";
|
||||
const CAPACITY_IDS = new Set(['server1', 'server2', 'server3']);
|
||||
let remaining = 12;
|
||||
|
||||
/* ===== Login banner (whoami) ===== */
|
||||
/* ===== Login banner (whoami) + Discord OAuth ===== */
|
||||
async function getWhoAmI() {
|
||||
try {
|
||||
// inventory endpoint should set/return session if already logged in
|
||||
const r = await fetch(`${API}/api/inventory`, { credentials: 'include' });
|
||||
if (!r.ok) return null;
|
||||
const j = await r.json();
|
||||
/* ===== Login banner (whoami) ===== */
|
||||
async function getWhoAmI() {
|
||||
try {
|
||||
const r = await fetch(`${API}/api/inventory`, { credentials: 'include' })
|
||||
if (!r.ok) return null;
|
||||
return await r.json();
|
||||
} catch { return null; }
|
||||
}
|
||||
getWhoAmI().then(u => {
|
||||
if (u) document.getElementById('whoami').textContent =
|
||||
`Signed in as ${u.global_name || u.username}`;
|
||||
});
|
||||
|
||||
/* ===== Products (unique IDs) ===== */
|
||||
const products = [
|
||||
{ id: 'server1', name: 'Server Class 1', price: 25.00, category: 'servers', tag: 'Server monthly', img: 'img/PrivateServerCLASS01.png'},
|
||||
{ id: 'server2', name: 'Server Class 2', price: 50.00, category: 'servers', tag: 'Server monthly', img: 'img/PrivateServerCLASS02.png'},
|
||||
{ id: 'server3', name: 'Server Class 3', price: 75.00, category: 'servers', tag: 'Server monthly', img: 'img/PrivateServerCLASS03.png' },
|
||||
|
||||
// Some backends return user data nested (e.g., { user: {...} })
|
||||
const u = j?.user ?? j;
|
||||
{ id: 'slot30', name: 'ASA Server Slot x30 days', price: 5.00, category: 'perks', tag: 'Server Perk monthly', img: 'img/ServerSlotX30.png' },
|
||||
{ id: 'mutants', name: 'Mutated Creatures', price: 1.50, category: 'bundles', tag: 'Dino Pack One Time Payment 10 non-breedable', img: 'img/MutatedCretures.png'},
|
||||
{ id: 'starter', name: 'Starter Pack', price: 1.50, category: 'bundles', tag: 'Starter Pack One Time Payment', img: 'img/StarterPack.png'},
|
||||
|
||||
// Prefer global_name, fall back to username, then to Discord ID
|
||||
const label =
|
||||
u?.global_name ??
|
||||
u?.username ??
|
||||
(u?.id ? `User ${u.id}` : null);
|
||||
{ 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'},
|
||||
|
||||
return label ? { label, raw: u } : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
{ 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' },
|
||||
];
|
||||
|
||||
// Show status next to the button
|
||||
(async () => {
|
||||
const who = await getWhoAmI();
|
||||
document.getElementById('whoami').textContent =
|
||||
who ? `Signed in as ${who.label}` : 'Not signed in';
|
||||
})();
|
||||
/* ===== State / helpers ===== */
|
||||
const state = { q: '', cat: 'all', sort: 'featured', cart: loadCart() };
|
||||
const $ = s => document.querySelector(s);
|
||||
const fmt = n => `$${n.toFixed(2)}`;
|
||||
function saveCart() { localStorage.setItem('obli.cart', JSON.stringify(state.cart)); }
|
||||
function loadCart() { try { return JSON.parse(localStorage.getItem('obli.cart') || '{}'); } catch { return {}; } }
|
||||
function cartCount() { return Object.values(state.cart).reduce((a, b) => a + b, 0); }
|
||||
function cartTotal() { return Object.entries(state.cart).reduce((s, [id, q]) => { const p = products.find(p => p.id === id); return s + (p ? p.price * q : 0) }, 0); }
|
||||
const isServer = id => id === 'server1' || id === 'server2' || id === 'server3';
|
||||
function serversInCart() {
|
||||
return Object.entries(state.cart || {})
|
||||
.reduce((n, [id, qty]) => n + (CAPACITY_IDS.has(id) ? Number(qty || 0) : 0), 0);
|
||||
}
|
||||
function canAddServers(qtyToAdd) {
|
||||
return serversInCart() + qtyToAdd <= remaining;
|
||||
}
|
||||
|
||||
// Clicking the button should start Discord OAuth on your server.
|
||||
// Adjust the path if your backend uses a different route.
|
||||
document.getElementById('loginDiscord').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const redirect = encodeURIComponent(location.href);
|
||||
location.href = `${API}/api/auth/discord?redirect=${redirect}`;
|
||||
});
|
||||
|
||||
|
||||
/* ===== Products (unique IDs) ===== */
|
||||
const products = [
|
||||
{ id: 'server1', name: 'Server Class 1', price: 25.00, category: 'servers', tag: 'Server monthly', img: 'img/PrivateServerCLASS01.png' },
|
||||
{ id: 'server2', name: 'Server Class 2', price: 50.00, category: 'servers', tag: 'Server monthly', img: 'img/PrivateServerCLASS02.png' },
|
||||
{ id: 'server3', name: 'Server Class 3', price: 75.00, category: 'servers', tag: 'Server monthly', img: 'img/PrivateServerCLASS03.png' },
|
||||
|
||||
{ id: 'slot30', name: 'ASA Server Slot x30 days', price: 5.00, category: 'perks', tag: 'Server Perk monthly', img: 'img/ServerSlotX30.png' },
|
||||
{ id: 'mutants', name: 'Mutated Creatures', price: 1.50, category: 'bundles', tag: 'Dino Pack One Time Payment 10 non-breedable', img: 'img/MutatedCretures.png' },
|
||||
{ id: 'starter', name: 'Starter Pack', price: 1.50, category: 'bundles', tag: 'Starter Pack One Time Payment', img: 'img/StarterPack.png' },
|
||||
|
||||
{ id: 'supporter1', name: 'ASA Server Supporter Pack 1', price: 5.00, category: 'vip', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 50,000 Points every week🎁 50,000 Bonus Points at the start of each wipe 🏅 Exclusive VIP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP1.png' },
|
||||
{ id: 'supporter2', name: 'ASA Server Supporter Pack 2', price: 10.00, category: 'vip', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 100,000 Points every week🎁 100,000 Bonus Points at the start of each wipe 🏅 Exclusive VIP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP2.png' },
|
||||
{ id: 'supporter3', name: 'ASA Server Supporter Pack 3', price: 15.00, category: 'vip', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 150,000 Points every week🎁 150,000 Bonus Points at the start of each wipe 🏅 Exclusive VIP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP3.png' },
|
||||
{ id: 'supporter4', name: 'ASA Server Supporter Pack 4', price: 20.00, category: 'vip', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 500,000 Points every week🎁 500,000 Bonus Points at the start of each wipe 🏅 Exclusive MVP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP4.png' },
|
||||
{ id: 'supporter5', name: 'ASA Server Supporter Pack 5', price: 25.00, category: 'vip', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 1,000,000 Points every week🎁 1,000,000 Bonus Points at the start of each wipe 🏅 Exclusive MVP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP5.png' },
|
||||
{ id: 'supporter6', name: 'ASA Server Supporter Pack 6', price: 35.00, category: 'vip', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 1,500,000 Points every week🎁 1,500,000 Bonus Points at the start of each wipe 🏅 Exclusive MVP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP6.png' },
|
||||
|
||||
{ id: 'map-small', name: 'Small Map manipulation', price: 5.00, category: 'maps', tag: 'Small Map manipulation Pack One Time Payment', img: 'img/SmallMap.png' },
|
||||
{ id: 'map-medium', name: 'Medium Map manipulation', price: 10.00, category: 'maps', tag: 'Medium Map manipulation Pack One Time Payment', img: 'img/MediumMap.png' },
|
||||
{ id: 'map-large', name: 'Large Map manipulation', price: 15.00, category: 'maps', tag: 'Large Map manipulation Pack One Time Payment', img: 'img/LargeMap.png' },
|
||||
];
|
||||
|
||||
/* ===== State / helpers ===== */
|
||||
const state = { q: '', cat: 'all', sort: 'featured', cart: loadCart() };
|
||||
const $ = s => document.querySelector(s);
|
||||
const fmt = n => `$${n.toFixed(2)}`;
|
||||
function saveCart() { localStorage.setItem('obli.cart', JSON.stringify(state.cart)); }
|
||||
function loadCart() { try { return JSON.parse(localStorage.getItem('obli.cart') || '{}'); } catch { return {}; } }
|
||||
function cartCount() { return Object.values(state.cart).reduce((a, b) => a + b, 0); }
|
||||
function cartTotal() { return Object.entries(state.cart).reduce((s, [id, q]) => { const p = products.find(p => p.id === id); return s + (p ? p.price * q : 0) }, 0); }
|
||||
const isServer = id => id === 'server1' || id === 'server2' || id === 'server3';
|
||||
function serversInCart() {
|
||||
return Object.entries(state.cart || {})
|
||||
.reduce((n, [id, qty]) => n + (CAPACITY_IDS.has(id) ? Number(qty || 0) : 0), 0);
|
||||
}
|
||||
function canAddServers(qtyToAdd) {
|
||||
return serversInCart() + qtyToAdd <= remaining;
|
||||
}
|
||||
|
||||
/* ===== Render ===== */
|
||||
/* ===== Render ===== */
|
||||
function cardHtml(p) {
|
||||
const remHtml = CAPACITY_IDS.has(p.id)
|
||||
? `<div class="rem-row">Remaining: <span class="remN" data-rem-for="${p.id}">12</span>/12</div>`
|
||||
|
|
@ -619,217 +531,183 @@
|
|||
if (p.name === 'Server Class 3') infoBtn = `<button class="btn btn-secondary" data-info="server-class-3">Info</button>`;
|
||||
|
||||
return `<article class="card" data-product="${p.id}">
|
||||
<div class="img">${p.img ? `<img src="${p.img}" alt="${p.name}">` : `<canvas data-id="${p.id}" width="320" height="150"></canvas>`}</div>
|
||||
<div class="footer">
|
||||
<h3>${p.name}</h3>
|
||||
<div class="meta">${p.tag || ''}</div>
|
||||
<div class="price">${fmt(p.price)}</div>
|
||||
<div class="chip">${p.category}</div>
|
||||
${remHtml}
|
||||
<div class="actions">
|
||||
<button class="btn" data-add="${p.id}">${inCart ? 'Add another' : 'Add to cart'}</button>
|
||||
${infoBtn}
|
||||
</div>
|
||||
</div>
|
||||
</article>`;
|
||||
<div class="img">${p.img ? `<img src="${p.img}" alt="${p.name}">` : `<canvas data-id="${p.id}" width="320" height="150"></canvas>`}</div>
|
||||
<div class="footer">
|
||||
<h3>${p.name}</h3>
|
||||
<div class="meta">${p.tag || ''}</div>
|
||||
<div class="price">${fmt(p.price)}</div>
|
||||
<div class="chip">${p.category}</div>
|
||||
${remHtml}
|
||||
<div class="actions">
|
||||
<button class="btn" data-add="${p.id}">${inCart ? 'Add another' : 'Add to cart'}</button>
|
||||
${infoBtn}
|
||||
</div>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function render() {
|
||||
const grid = $('#grid');
|
||||
let list = products.filter(p => state.cat === 'all' || p.category === state.cat);
|
||||
if (state.q) {
|
||||
const q = state.q.toLowerCase();
|
||||
list = list.filter(p =>
|
||||
p.name.toLowerCase().includes(q) ||
|
||||
(p.tag || '').toLowerCase().includes(q) ||
|
||||
(p.category || '').toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
switch (state.sort) {
|
||||
case 'price-asc': list.sort((a, b) => a.price - b.price); break;
|
||||
case 'price-desc': list.sort((a, b) => b.price - a.price); break;
|
||||
case 'name': list.sort((a, b) => a.name.localeCompare(b.name)); break;
|
||||
}
|
||||
grid.innerHTML = list.map(cardHtml).join('');
|
||||
refreshRemaining();
|
||||
updateCartUi();
|
||||
}
|
||||
|
||||
/* ===== Cart UI ===== */
|
||||
function rowHtml(id, qty) {
|
||||
const p = products.find(x => x.id === id);
|
||||
return `<div class="row">
|
||||
<div class="title"><div style="font-weight:600">${p.name}</div><div class="meta">${fmt(p.price)} each</div></div>
|
||||
<div class="qty">
|
||||
<button data-dec="${id}">−</button>
|
||||
<div>${qty}</div>
|
||||
<button data-inc="${id}">+</button>
|
||||
</div>
|
||||
<div style="width:72px; text-align:right">${fmt(p.price * qty)}</div>
|
||||
</div>`;
|
||||
}
|
||||
function updateCartUi() {
|
||||
$('#cartCount').textContent = cartCount();
|
||||
$('#cartTotal').textContent = fmt(cartTotal());
|
||||
const rows = Object.entries(state.cart);
|
||||
$('#cartItems').innerHTML = rows.length ? rows.map(([id, qty]) => rowHtml(id, qty)).join('') : '<div class="muted">Cart is empty.</div>';
|
||||
|
||||
|
||||
}
|
||||
|
||||
/* ===== Events ===== */
|
||||
$('#search').addEventListener('input', e => { state.q = e.target.value; render(); });
|
||||
$('#category').addEventListener('change', e => { state.cat = e.target.value; render(); });
|
||||
$('#sort').addEventListener('change', e => { state.sort = e.target.value; render(); });
|
||||
|
||||
document.body.addEventListener('click', e => {
|
||||
const add = e.target.closest('[data-add]');
|
||||
const inc = e.target.closest('[data-inc]');
|
||||
const dec = e.target.closest('[data-dec]');
|
||||
|
||||
if (add) {
|
||||
const id = add.getAttribute('data-add');
|
||||
const qtyToAdd = 1;
|
||||
if (isServer(id) && !canAddServers(qtyToAdd)) {
|
||||
alert(`Sold out or limit reached. Remaining: ${Math.max(0, remaining - serversInCart())}.`);
|
||||
return;
|
||||
function render() {
|
||||
const grid = $('#grid');
|
||||
let list = products.filter(p => state.cat === 'all' || p.category === state.cat);
|
||||
if (state.q) {
|
||||
const q = state.q.toLowerCase();
|
||||
list = list.filter(p =>
|
||||
p.name.toLowerCase().includes(q) ||
|
||||
(p.tag || '').toLowerCase().includes(q) ||
|
||||
(p.category || '').toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
switch (state.sort) {
|
||||
case 'price-asc': list.sort((a, b) => a.price - b.price); break;
|
||||
case 'price-desc': list.sort((a, b) => b.price - a.price); break;
|
||||
case 'name': list.sort((a, b) => a.name.localeCompare(b.name)); break;
|
||||
}
|
||||
grid.innerHTML = list.map(cardHtml).join('');
|
||||
refreshRemaining();
|
||||
updateCartUi();
|
||||
}
|
||||
state.cart[id] = (state.cart[id] || 0) + 1;
|
||||
saveCart(); render(); refreshRemaining();
|
||||
|
||||
}
|
||||
|
||||
if (inc) {
|
||||
const id = inc.getAttribute('data-inc');
|
||||
const qtyToAdd = 1;
|
||||
if (isServer(id) && !canAddServers(qtyToAdd)) {
|
||||
alert(`Sold out or limit reached. Remaining: ${Math.max(0, remaining - serversInCart())}.`);
|
||||
return;
|
||||
/* ===== 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>`;
|
||||
}
|
||||
state.cart[id] = (state.cart[id] || 0) + 1;
|
||||
saveCart(); updateCartUi(); refreshRemaining();
|
||||
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>';
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (dec) {
|
||||
const id = dec.getAttribute('data-dec');
|
||||
state.cart[id] = Math.max(0, (state.cart[id] || 0) - 1);
|
||||
if (state.cart[id] === 0) delete state.cart[id];
|
||||
saveCart(); render(); refreshRemaining();
|
||||
/* ===== Events ===== */
|
||||
$('#search').addEventListener('input', e => { state.q = e.target.value; render(); });
|
||||
$('#category').addEventListener('change', e => { state.cat = e.target.value; render(); });
|
||||
$('#sort').addEventListener('change', e => { state.sort = e.target.value; render(); });
|
||||
|
||||
}
|
||||
});
|
||||
document.body.addEventListener('click', e => {
|
||||
const add = e.target.closest('[data-add]');
|
||||
const inc = e.target.closest('[data-inc]');
|
||||
const dec = e.target.closest('[data-dec]');
|
||||
|
||||
if (add) {
|
||||
const id = add.getAttribute('data-add');
|
||||
const qtyToAdd = 1;
|
||||
if (isServer(id) && !canAddServers(qtyToAdd)) {
|
||||
alert(`Sold out or limit reached. Remaining: ${Math.max(0, remaining - serversInCart())}.`);
|
||||
return;
|
||||
}
|
||||
state.cart[id] = (state.cart[id] || 0) + 1;
|
||||
saveCart(); render(); refreshRemaining();
|
||||
|
||||
}
|
||||
|
||||
if (inc) {
|
||||
const id = inc.getAttribute('data-inc');
|
||||
const qtyToAdd = 1;
|
||||
if (isServer(id) && !canAddServers(qtyToAdd)) {
|
||||
alert(`Sold out or limit reached. Remaining: ${Math.max(0, remaining - serversInCart())}.`);
|
||||
return;
|
||||
}
|
||||
state.cart[id] = (state.cart[id] || 0) + 1;
|
||||
saveCart(); updateCartUi(); refreshRemaining();
|
||||
|
||||
}
|
||||
|
||||
if (dec) {
|
||||
const id = dec.getAttribute('data-dec');
|
||||
state.cart[id] = Math.max(0, (state.cart[id] || 0) - 1);
|
||||
if (state.cart[id] === 0) delete state.cart[id];
|
||||
saveCart(); render(); refreshRemaining();
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/* ===== Modals (single global handlers) ===== */
|
||||
const modals = {
|
||||
'server-class-1': document.getElementById('infoModal'),
|
||||
'server-class-2': document.getElementById('infoModal2'),
|
||||
'server-class-3': document.getElementById('infoModal3'),
|
||||
};
|
||||
document.body.addEventListener('click', (e) => {
|
||||
const infoBtn = e.target.closest('[data-info]');
|
||||
if (infoBtn) {
|
||||
const k = infoBtn.getAttribute('data-info');
|
||||
const m = modals[k];
|
||||
if (m) { m.classList.add('open'); m.setAttribute('aria-hidden', 'false'); }
|
||||
}
|
||||
const closeBtn = e.target.closest('.modal__close');
|
||||
if (closeBtn) {
|
||||
const id = closeBtn.getAttribute('data-close');
|
||||
const m = id ? document.getElementById(id) : closeBtn.closest('.modal');
|
||||
if (m) { m.classList.remove('open'); m.setAttribute('aria-hidden', 'true'); }
|
||||
}
|
||||
});
|
||||
Object.values(modals).forEach(m => {
|
||||
if (!m) return;
|
||||
m.addEventListener('click', (e) => { if (e.target === m) { m.classList.remove('open'); m.setAttribute('aria-hidden', 'true'); } });
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') { Object.values(modals).forEach(m => m?.classList.remove('open')); }
|
||||
});
|
||||
/* ===== 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 ===== */
|
||||
/* ===== Drawer ===== */
|
||||
const drawer = document.getElementById('drawer');
|
||||
document.getElementById('openCart').addEventListener('click', e => {
|
||||
e.preventDefault(); drawer.classList.add('open'); drawer.setAttribute('aria-hidden', 'false'); updateCartUi();
|
||||
});
|
||||
document.getElementById('exitCart').addEventListener('click', () => {
|
||||
drawer.classList.remove('open'); drawer.setAttribute('aria-hidden', 'true');
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') { drawer.classList.remove('open'); drawer.setAttribute('aria-hidden', 'true'); }
|
||||
});
|
||||
|
||||
/* ===== Checkout ===== */
|
||||
|
||||
|
||||
async function checkout(cart) {
|
||||
let user = null;
|
||||
try {
|
||||
const r = await fetch(`${API}/api/inventory`, { credentials: 'include' });
|
||||
if (r.ok) user = await r.json();
|
||||
} catch { }
|
||||
|
||||
// reserve
|
||||
// 1) reserve
|
||||
const r1 = await fetch(`${API}/api/reserve`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ items: cart, user })
|
||||
body: JSON.stringify({ items: cart })
|
||||
});
|
||||
const j1 = await r1.json();
|
||||
if (!j1.ok) { alert(j1.error || 'reserve failed'); return; }
|
||||
|
||||
// create checkout with Discord user
|
||||
// 2) create checkout
|
||||
const r2 = await fetch(`${API}/api/create-checkout`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
resKey: j1.resKey,
|
||||
items: [],
|
||||
discordUser: user, // <— this flows to metadata
|
||||
success_url: location.origin + location.pathname + '?ok=1',
|
||||
cancel_url: location.origin + location.pathname + '?cancel=1'
|
||||
})
|
||||
});
|
||||
const j2 = await r2.json();
|
||||
if (!j2.url) { alert(j2.error || 'create-checkout failed'); return; }
|
||||
location.href = j2.url;
|
||||
location.href = j2.url; // redirect to Stripe
|
||||
}
|
||||
|
||||
|
||||
// Single, unified checkout click handler (shows warning, then proceeds)
|
||||
document.getElementById('checkoutBtn').addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const entries = Object.entries(state.cart || {});
|
||||
if (!entries.length) return alert('Your cart is empty.');
|
||||
|
||||
alert("⚠️ IMPORTANT: After checkout, you must contact a team member to receive your order.\n\nPlease open a ticket in Discord once payment is complete.");
|
||||
|
||||
const items = entries.map(([id, qty]) => {
|
||||
const p = products.find(p => p.id === id);
|
||||
return { id, name: p?.name || id, qty: Number(qty || 1), price: p?.price || 0 };
|
||||
});
|
||||
|
||||
await checkout(items);
|
||||
});
|
||||
|
||||
// === Checkout warning ===
|
||||
document.getElementById('checkoutBtn').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
alert("⚠️ IMPORTANT: After checkout, you must contact a team member to receive your order.\n\nPlease open a ticket in Discord once payment is complete.");
|
||||
});
|
||||
|
||||
document.getElementById('checkoutBtn').addEventListener('click', async () => {
|
||||
const entries = Object.entries(state.cart || {});
|
||||
if (!entries.length) return alert('Your cart is empty.');
|
||||
|
||||
alert("⚠️ IMPORTANT: After checkout, you must contact a team member to receive your order.\n\nPlease open a ticket in Discord once payment is complete.");
|
||||
|
||||
const items = entries.map(([id, qty]) => {
|
||||
const p = products.find(p => p.id === id);
|
||||
return { id, name: p?.name || id, qty: Number(qty || 1), price: p?.price || 0 };
|
||||
|
|
@ -842,28 +720,28 @@
|
|||
|
||||
|
||||
|
||||
/* ===== 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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* ===== Boot ===== */
|
||||
document.getElementById('year').textContent = new Date().getFullYear();
|
||||
render();
|
||||
paintPlaceholders();
|
||||
|
||||
/* ===== Placeholder art for items without images ===== */
|
||||
function paintPlaceholders() {
|
||||
document.querySelectorAll('canvas[data-id]').forEach(cv => {
|
||||
const id = cv.getAttribute('data-id');
|
||||
const ctx = cv.getContext('2d');
|
||||
const g = ctx.createLinearGradient(0, 0, cv.width, cv.height);
|
||||
g.addColorStop(0, '#0f1b3a'); g.addColorStop(1, '#1b2f5d');
|
||||
ctx.fillStyle = g; ctx.fillRect(0, 0, cv.width, cv.height);
|
||||
ctx.strokeStyle = 'rgba(255,255,255,.12)';
|
||||
for (let i = 0; i < 7; i++) { ctx.beginPath(); ctx.moveTo(0, 22 * i); ctx.lineTo(cv.width, 22 * i); ctx.stroke(); }
|
||||
ctx.fillStyle = '#aad0ff'; ctx.font = 'bold 16px Inter,system-ui';
|
||||
ctx.fillText(products.find(p => p.id === id)?.name || id, 12, cv.height - 12);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async function refreshRemaining() {
|
||||
try {
|
||||
|
|
@ -885,7 +763,7 @@
|
|||
addBtn.disabled = left <= 0;
|
||||
addBtn.textContent = left <= 0 ? 'Sold out' : 'Add to cart';
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
el.textContent = '∞';
|
||||
if (addBtn) {
|
||||
|
|
@ -898,7 +776,7 @@
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Reference in New Issue