Add files via upload

This commit is contained in:
James 2025-10-01 23:42:17 -07:00 committed by GitHub
parent 980e9d2322
commit 824a46172e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 431 additions and 122 deletions

View File

@ -8,6 +8,83 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
/* Secondary button for Info */
.btn-secondary {
background: transparent;
color: var(--text);
border: 1px solid rgba(255,255,255,.18);
}
.btn-secondary:hover {
background: rgba(255,255,255,.06);
color: var(--text);
}
/* Modal */
.modal {
position: fixed;
inset: 0;
display: none;
background: rgba(0,0,0,.55);
z-index: 80;
}
.modal.open {
display: grid;
place-items: center;
}
.modal__dialog {
width: min(680px, 92vw);
background: var(--panel);
border: 1px solid rgba(255,255,255,.08);
border-radius: var(--radius);
box-shadow: 0 20px 80px rgba(0,0,0,.5);
}
.modal__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid rgba(255,255,255,.08);
}
.modal__body {
padding: 16px;
color: var(--text);
}
.modal__close {
width: 32px;
height: 32px;
border-radius: 8px;
border: 1px solid rgba(255,255,255,.15);
background: transparent;
color: var(--text);
font-size: 18px;
cursor: pointer;
}
.modal__close:hover {
background: rgba(255,255,255,.06);
}
.exit-btn {
background: var(--bad);
color: #fff;
border: none;
font-size: 13px;
font-weight: 600;
padding: 4px 10px;
border-radius: 6px;
cursor: pointer;
}
.exit-btn:hover {
filter: brightness(1.1);
}
:root {
--bg: #0b1220; /* deep space */
--panel: #0f1730; /* cards/nav */
@ -333,7 +410,7 @@
<a href="https://www.oblistudios.com">ObliStudios</a>
</div>
<a href="https://www.oblistudios.com/ASAservers.html">Servers</a>
<div class="spacer"></div>
<a class="cart-btn" href="#" id="openCart">🛒 <span id="cartCount">0</span></a>
</nav>
@ -348,10 +425,9 @@
<select id="category">
<option value="all">All categories</option>
<option value="merch">Merch</option>
<option value="server">Servers</option>
<option value="servers">Servers</option>
<option value="perks">Server Perks</option>
</select>
<select id="sort">
<option value="featured">Sort: Featured</option>
<option value="price-asc">Price: Low → High</option>
@ -364,8 +440,9 @@
</main>
<aside class="drawer" id="drawer" aria-hidden="true">
<header>
<h4>Cart</h4>
<header style="display:flex;align-items:center;justify-content:space-between;align-items:center">
<h4 style="margin:0">Cart</h4>
<button id="exitCart" class="exit-btn">Exit</button>
</header>
<div class="items" id="cartItems"></div>
<div class="foot">
@ -374,150 +451,382 @@
<div id="cartTotal" class="price">$0.00</div>
</div>
<button class="checkout" id="checkoutBtn">Checkout</button>
<div style="margin-top:8px; font-size:12px; color:var(--muted)">Checkout uses Stripe Payment Links per item or a generic link. Configure below in <code>products</code>.</div>
<div style="margin-top:8px; font-size:12px; color:var(--muted)">
Checkout uses Stripe Payment Links per item or a generic link. Configure below in <code>products</code>.
</div>
</div>
</aside>
<footer>
<div>© <span id="year"></span> ObliStudios </div>
</footer>
<script>
// ======= Demo products (edit to your real products) =======
const products = [
{ id: 'Server', name: 'Server Class 1', price: 25.00, category: 'server', tag: 'Server monthly', img: 'img/PrivateServerCLASS01.png', payment: 'https://buy.stripe.com/test_123tee' },
{ id: 'Server', name: 'Server Class 2', price: 50.00, category: 'server', tag: 'Server monthly', img: 'img/PrivateServerCLASS02.png', payment: 'https://buy.stripe.com/test_123mug' },
{ id: 'Server', name: 'Server Class 3', price: 75.00, category: 'server', tag: 'Server monthly', img: 'img/PrivateServerCLASS03.png', payment: 'https://buy.stripe.com/test_123ost' },
{ id: 'perk1', name: 'ASA Server Slot x30 days', price: 5.00, category: 'perks', tag: 'Server Perk monthly', img: 'img/ServerSlotX30.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'perk1', name: 'Mutated Creatures', price: 1.50, category: 'perks', tag: 'Dino Pack One Time Payment 10 non-breedable', img: 'img/MutatedCretures.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'perk1', name: 'Starter Pack', price: 1.50, category: 'perks', tag: 'Starter Pack One Time Payment', img: 'img/StarterPack.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'Server', name: 'Small Map manipulation', price: 5.00, category: 'server', tag: 'Small Map manipulation Pack One Time Payment', img: 'img/SmallMap.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'Server', name: 'Medium Map manipulation', price: 10.00, category: 'server', tag: 'Medium Map manipulation Pack One Time Payment', img: 'img/MediumMap.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'Server', name: 'Large Map manipulation', price: 15.00, category: 'server', tag: 'Large Map manipulation Pack One Time Payment', img: 'img/LargeMap.png', payment: 'https://buy.stripe.com/test_123perk' },
];
// ======= Demo products (edit to your real products) =======
// ======= State =======
const state = { q:'', cat:'all', sort:'featured', cart: loadCart() };
// ======= Helpers =======
const $ = sel => document.querySelector(sel);
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((sum,[id,qty])=>{
const p = products.find(p=>p.id===id); return sum + (p?p.price*qty:0);
},0); }
const products = [
{ id: 'server1', name: 'Server Class 1', price: 25.00, category: 'servers', tag: 'Server monthly', img: 'img/PrivateServerCLASS01.png', payment: 'https://buy.stripe.com/test_123tee' },
{ id: 'server2', name: 'Server Class 2', price: 50.00, category: 'servers', tag: 'Server monthly', img: 'img/PrivateServerCLASS02.png', payment: 'https://buy.stripe.com/test_123mug' },
{ id: 'server3', name: 'Server Class 3', price: 75.00, category: 'servers', tag: 'Server monthly', img: 'img/PrivateServerCLASS03.png', payment: 'https://buy.stripe.com/test_123ost' },
// ======= Render products =======
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)); }
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;
default: /* featured */ break;
}
grid.innerHTML = list.map(p=>cardHtml(p)).join('');
updateCartUi();
}
{ id: 'slot30', name: 'ASA Server Slot x30 days', price: 5.00, category: 'perks', tag: 'Server Perk monthly', img: 'img/ServerSlotX30.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'mutants', name: 'Mutated Creatures', price: 1.50, category: 'perks', tag: 'Dino Pack One Time Payment 10 non-breedable', img: 'img/MutatedCretures.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'starter', name: 'Starter Pack', price: 1.50, category: 'perks', tag: 'Starter Pack One Time Payment', img: 'img/StarterPack.png', payment: 'https://buy.stripe.com/test_123perk' },
function cardHtml(p){
const inCart = state.cart[p.id]||0;
return `<article class="card">
<div class="img">${p.img?`<img src="${p.img}" alt="${p.name}">`:`<canvas data-id="${p.id}" width="320" height="160"></canvas>`}</div>
{ id: 'supporter1', name: 'ASA Server Supporter Pack 1', price: 5.00, category: 'perks', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 50,000 Points every week🎁 50,000 Bonus Points at the start of each wipe 🏅 Exclusive VIP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP1.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'supporter2', name: 'ASA Server Supporter Pack 2', price: 10.00, category: 'perks', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 100,000 Points every week🎁 100,000 Bonus Points at the start of each wipe 🏅 Exclusive VIP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP2.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'supporter3', name: 'ASA Server Supporter Pack 3', price: 15.00, category: 'perks', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 150,000 Points every week🎁 150,000 Bonus Points at the start of each wipe 🏅 Exclusive VIP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP3.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'supporter4', name: 'ASA Server Supporter Pack 4', price: 20.00, category: 'perks', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 500,000 Points every week🎁 500,000 Bonus Points at the start of each wipe 🏅 Exclusive MVP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP4.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'supporter5', name: 'ASA Server Supporter Pack 5', price: 25.00, category: 'perks', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 1,000,000 Points every week🎁 1,000,000 Bonus Points at the start of each wipe 🏅 Exclusive MVP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP5.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'supporter6', name: 'ASA Server Supporter Pack 6', price: 35.00, category: 'perks', tag: 'Server Perk monthly <br> ✨ Perks Included ✨ 💎 1,500,000 Points every week🎁 1,500,000 Bonus Points at the start of each wipe 🏅 Exclusive MVP Role on Discord 🔒 Access to the Donator - Only Chat', img: 'img/SP6.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'map-small', name: 'Small Map manipulation', price: 5.00, category: 'servers', tag: 'Small Map manipulation Pack One Time Payment', img: 'img/SmallMap.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'map-medium', name: 'Medium Map manipulation', price: 10.00, category: 'servers', tag: 'Medium Map manipulation Pack One Time Payment', img: 'img/MediumMap.png', payment: 'https://buy.stripe.com/test_123perk' },
{ id: 'map-large', name: 'Large Map manipulation', price: 15.00, category: 'servers', tag: 'Large Map manipulation Pack One Time Payment', img: 'img/LargeMap.png', payment: 'https://buy.stripe.com/test_123perk' },
];
// ======= State =======
const state = { q: '', cat: 'all', sort: 'featured', cart: loadCart() };
// ======= Helpers =======
const $ = sel => document.querySelector(sel);
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((sum, [id, qty]) => {
const p = products.find(p => p.id === id); return sum + (p ? p.price * qty : 0);
}, 0);
}
// ======= Render products =======
function render() {
const grid = $('#grid');
// Base filter by category
let list = products.filter(p => state.cat === 'all' || p.category === state.cat);
// Search by name, tag, OR category
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)
);
}
// Sorting
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;
default: /* featured */ break;
}
grid.innerHTML = list.map(p => cardHtml(p)).join('');
updateCartUi();
}
function cardHtml(p) {
const inCart = state.cart[p.id] || 0;
let infoBtn = '';
if (p.name === 'Server Class 1') {
infoBtn = `<button class="btn btn-secondary" data-info="server-class-1">Info</button>`;
}
if (p.name === 'Server Class 2') {
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>`;
}
return `<article class="card">
<div class="img">${p.img ? `<img src="${p.img}" alt="${p.name}">` : `<canvas data-id="${p.id}" width="320" height="160"></canvas>`}</div>
<div class="body">
<h3>${p.name}</h3>
<div class="meta">${p.tag || ''}</div>
<div class="price" style="margin-top:6px">${fmt(p.price)}</div>
<div class="chip">${p.category}</div>
<button class="btn" data-add="${p.id}">${inCart?`Add another`:`Add to cart`}</button>
<div style="display:flex; gap:8px; flex-wrap:wrap">
<button class="btn" data-add="${p.id}">${inCart ? `Add another` : `Add to cart`}</button>
${infoBtn}
</div>
</div>
</article>`;
}
}
// ======= Cart UI =======
function updateCartUi(){
$('#cartCount').textContent = cartCount();
$('#cartTotal').textContent = fmt(cartTotal());
const wrap = $('#cartItems');
const rows = Object.entries(state.cart);
wrap.innerHTML = rows.length? rows.map(([id,qty])=> rowHtml(id,qty)).join('') : '<div style="color:var(--muted)">Cart is empty.</div>';
}
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>`;
}
// ======= 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(); });
// ======= Cart UI =======
function updateCartUi() {
$('#cartCount').textContent = cartCount();
$('#cartTotal').textContent = fmt(cartTotal());
const wrap = $('#cartItems');
const rows = Object.entries(state.cart);
wrap.innerHTML = rows.length ? rows.map(([id, qty]) => rowHtml(id, qty)).join('') : '<div style="color:var(--muted)">Cart is empty.</div>';
}
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'); state.cart[id]=(state.cart[id]||0)+1; saveCart(); render(); }
if(inc){ const id = inc.getAttribute('data-inc'); state.cart[id]=(state.cart[id]||0)+1; saveCart(); updateCartUi(); }
if(dec){ const id = dec.getAttribute('data-dec'); state.cart[id]=Math.max(0,(state.cart[id]||0)-1); if(state.cart[id]===0) delete state.cart[id]; saveCart(); render(); }
});
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>`;
}
const drawer = $('#drawer');
$('#openCart').addEventListener('click', e=>{ e.preventDefault(); drawer.classList.add('open'); drawer.setAttribute('aria-hidden','false'); updateCartUi(); });
document.addEventListener('keydown', e=>{ if(e.key==='Escape') { drawer.classList.remove('open'); drawer.setAttribute('aria-hidden','true'); }});
// ======= 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(); });
// Checkout strategy: if a single item, go to its Stripe Payment Link
// otherwise, open a generic link (configure below) or email summary.
const GENERIC_CHECKOUT = 'https://buy.stripe.com/test_generic';
$('#checkoutBtn').addEventListener('click', ()=>{
const entries = Object.entries(state.cart);
if(!entries.length) return alert('Your cart is empty.');
if(entries.length===1){
const [id, qty] = entries[0];
const p = products.find(p=>p.id===id);
if(p && p.payment){ window.open(p.payment, '_blank'); return; }
}
// Fallback: generic link (Stripe payment link for a bundle) or mailto
if(GENERIC_CHECKOUT && GENERIC_CHECKOUT.startsWith('http')){
window.open(GENERIC_CHECKOUT, '_blank');
} else {
const lines = entries.map(([id,qty])=>{ const p=products.find(p=>p.id===id); return `${qty} x ${p?.name || id}`; }).join('%0A');
window.location.href = `mailto:sales@oblistudios.com?subject=Order%20Request&body=${lines}`;
}
});
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'); state.cart[id] = (state.cart[id] || 0) + 1; saveCart(); render(); }
if (inc) { const id = inc.getAttribute('data-inc'); state.cart[id] = (state.cart[id] || 0) + 1; saveCart(); updateCartUi(); }
if (dec) { const id = dec.getAttribute('data-dec'); state.cart[id] = Math.max(0, (state.cart[id] || 0) - 1); if (state.cart[id] === 0) delete state.cart[id]; saveCart(); render(); }
// Info modal
const modal = $('#infoModal');
const infoClose = $('#infoClose');
// Minimal placeholder thumbnails (generated on canvas so page can ship without assets)
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,.15)';
for(let i=0;i<8;i++){ ctx.beginPath(); ctx.moveTo(0,i*22); ctx.lineTo(cv.width, i*22); ctx.stroke(); }
ctx.fillStyle = '#8cc4ff';
ctx.font = 'bold 18px Inter';
ctx.fillText(products.find(p=>p.id===id)?.name || id, 16, cv.height-16);
});
}
const modals = {
'server-class-1': $('#infoModal'),
'server-class-2': $('#infoModal2'),
'server-class-3': $('#infoModal3')
};
document.body.addEventListener('click', (e) => {
const infoBtn = e.target.closest('[data-info]');
if (infoBtn) {
const key = infoBtn.getAttribute('data-info');
const modal = modals[key];
if (modal) {
modal.classList.add('open');
modal.setAttribute('aria-hidden', 'false');
}
}
const closeBtn = e.target.closest('.modal__close');
if (closeBtn) {
const id = closeBtn.getAttribute('data-close');
const modal = id ? document.getElementById(id) : closeBtn.closest('.modal');
if (modal) {
modal.classList.remove('open');
modal.setAttribute('aria-hidden', 'true');
}
}
});
// Backdrop click + ESC close
Object.values(modals).forEach(modal => {
modal.addEventListener('click', e => {
if (e.target === modal) {
modal.classList.remove('open');
modal.setAttribute('aria-hidden', 'true');
}
});
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
Object.values(modals).forEach(modal => {
if (modal.classList.contains('open')) {
modal.classList.remove('open');
modal.setAttribute('aria-hidden', 'true');
}
});
}
});
infoClose.addEventListener('click', () => {
modal.classList.remove('open');
modal.setAttribute('aria-hidden', 'true');
});
modal.addEventListener('click', (e) => {
if (e.target === modal) { // click backdrop
modal.classList.remove('open');
modal.setAttribute('aria-hidden', 'true');
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal.classList.contains('open')) {
modal.classList.remove('open');
modal.setAttribute('aria-hidden', 'true');
}
});
});
const drawer = $('#drawer');
$('#openCart').addEventListener('click', e => { e.preventDefault(); drawer.classList.add('open'); drawer.setAttribute('aria-hidden', 'false'); updateCartUi(); });
document.addEventListener('keydown', e => { if (e.key === 'Escape') { drawer.classList.remove('open'); drawer.setAttribute('aria-hidden', 'true'); } });
// Checkout strategy: if a single item, go to its Stripe Payment Link
// otherwise, open a generic link (configure below) or email summary.
const GENERIC_CHECKOUT = 'https://buy.stripe.com/test_generic';
$('#checkoutBtn').addEventListener('click', () => {
const entries = Object.entries(state.cart);
if (!entries.length) return alert('Your cart is empty.');
if (entries.length === 1) {
const [id, qty] = entries[0];
const p = products.find(p => p.id === id);
if (p && p.payment) { window.open(p.payment, '_blank'); return; }
}
// Fallback: generic link (Stripe payment link for a bundle) or mailto
if (GENERIC_CHECKOUT && GENERIC_CHECKOUT.startsWith('http')) {
window.open(GENERIC_CHECKOUT, '_blank');
} else {
const lines = entries.map(([id, qty]) => { const p = products.find(p => p.id === id); return `${qty} x ${p?.name || id}`; }).join('%0A');
window.location.href = `mailto:sales@oblistudios.com?subject=Order%20Request&body=${lines}`;
}
});
// Minimal placeholder thumbnails (generated on canvas so page can ship without assets)
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,.15)';
for (let i = 0; i < 8; i++) { ctx.beginPath(); ctx.moveTo(0, i * 22); ctx.lineTo(cv.width, i * 22); ctx.stroke(); }
ctx.fillStyle = '#8cc4ff';
ctx.font = 'bold 18px Inter';
ctx.fillText(products.find(p => p.id === id)?.name || id, 16, cv.height - 16);
});
}
// Boot
document.getElementById('year').textContent = new Date().getFullYear();
render();
paintPlaceholders();
$('#exitCart').addEventListener('click', () => {
drawer.classList.remove('open');
drawer.setAttribute('aria-hidden', 'true');
});
// Boot
document.getElementById('year').textContent = new Date().getFullYear();
render();
paintPlaceholders();
</script>
<!-- Info Modal -->
<div id="infoModal" class="modal" aria-hidden="true">
<div class="modal__dialog" role="dialog" aria-modal="true" aria-labelledby="infoTitle">
<header class="modal__head">
<h3 id="infoTitle" style="margin:0">🔒 Private Server Tier</h3>
<button class="modal__close" id="infoClose" aria-label="Close">×</button>
</header>
<div class="modal__body">
<p><strong>Exclusive to you and your tribe.</strong></p>
<p>🏝 <strong>Server Setup:</strong> Private servers mirror the main cluster settings (PVP, breeding, rates). Each server supports <strong>20 player slots</strong> and is secured with a <strong>password of your choice</strong>.</p>
<p>⚙️ <strong>Tools & Perks:</strong> Includes the <strong>Creature Management Tool</strong> for bulk tame management and <strong>10× Mutation Potions</strong> per tier.</p>
<p>🎁 <strong>Starting Bonus:</strong> <strong>100,000 points</strong> granted to the server owner at the start of each wipe.</p>
<p><strong>Setup Time:</strong> Usually ready within <strong>2 business days</strong> after purchase. Servers never wipe while your subscription is active.</p>
<p>🏗 <strong>Building Rules:</strong> All standard rules apply, except for blocking obelisks. Otherwise, build anywhere.</p>
<p>
📜 <strong>Ownership Rules:</strong><br>
• Server is for you and your tribe only.<br>
• No outsider storage, griefing, or cluster disruption.<br>
• Owners must also maintain a base on at least one main server.
</p>
<p>🌐 <strong>Clustering:</strong> Linked to the main cluster within <strong>1 hour</strong> of any wipe.</p>
<p>💬 <strong>Support:</strong> After purchase, create a ticket on our <strong>Discord</strong> for setup and support.</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">
<div class="modal__body">
<p><strong>Exclusive to you and your tribe.</strong></p>
<p>🏝 <strong>Server Setup:</strong> Private servers mirror the main cluster settings (PVP, breeding, rates). Each server supports <strong>30 player slots</strong> and is secured with a <strong>password of your choice</strong>.</p>
<p>⚙️ <strong>Tools & Perks:</strong> Includes the <strong>Creature Management Tool</strong> for bulk tame management and <strong>10× Mutation Potions</strong> per tier.</p>
<p>🎁 <strong>Starting Bonus:</strong> <strong>200,000 points</strong> granted to the server owner at the start of each wipe.</p>
<p><strong>Setup Time:</strong> Usually ready within <strong>2 business days</strong> after purchase. Servers never wipe while your subscription is active.</p>
<p>🏗 <strong>Building Rules:</strong> All standard rules apply, except for blocking obelisks. Otherwise, build anywhere.</p>
<p>
📜 <strong>Ownership Rules:</strong><br>
• Server is for you and your tribe only.<br>
• No outsider storage, griefing, or cluster disruption.<br>
• Owners must also maintain a base on at least one main server.
</p>
<p>🌐 <strong>Clustering:</strong> Linked to the main cluster within <strong>1 hour</strong> of any wipe.</p>
<p>💬 <strong>Support:</strong> After purchase, create a ticket on our <strong>Discord</strong> for setup and support.</p>
</div>
</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">
<div class="modal__body">
<p><strong>Exclusive to you and your tribe.</strong></p>
<p>🏝 <strong>Server Setup:</strong> Private servers mirror the main cluster settings (PVP, breeding, rates). Each server supports <strong>50 player slots</strong> and is secured with a <strong>password of your choice</strong>.</p>
<p>⚙️ <strong>Tools & Perks:</strong> Includes the <strong>Creature Management Tool</strong> for bulk tame management and <strong>10× Mutation Potions</strong> per tier.</p>
<p>🎁 <strong>Starting Bonus:</strong> <strong>300,000 points</strong> granted to the server owner at the start of each wipe.</p>
<p><strong>Setup Time:</strong> Usually ready within <strong>2 business days</strong> after purchase. Servers never wipe while your subscription is active.</p>
<p>🏗 <strong>Building Rules:</strong> All standard rules apply, except for blocking obelisks. Otherwise, build anywhere.</p>
<p>
📜 <strong>Ownership Rules:</strong><br>
• Server is for you and your tribe only.<br>
• No outsider storage, griefing, or cluster disruption.<br>
• Owners must also maintain a base on at least one main server.
</p>
<p>🌐 <strong>Clustering:</strong> Linked to the main cluster within <strong>1 hour</strong> of any wipe.</p>
<p>💬 <strong>Support:</strong> After purchase, create a ticket on our <strong>Discord</strong> for setup and support.</p>
</div>
</div>
</div>
</div>
</body>
</html>