// USD prices. Add Shopify IDs for the base systems and upgrades, not every // possible combination. Each ID is the purchasable variant of that one item. // Estimated installed weights in lbs, per unit (not shipping weights). const ddr5Upgrade = { price: 8000, weight: 0.2, id: '67533223985370' }; // One 192 GB DDR5 upgrade. const platforms = [ { name: 'G4', ddr: 'DDR4', cpu: 'EPYC', pcie: 'PCIe 4', networking: '2× 1GbE + OCP3.0', base: { price: 7000, weight: 42, id: '50492208677082' }, memory: [ { gb: 32, price: 0 }, { gb: 128, price: 3000, weight: 0.2, id: '67533222641882' }, ] }, { name: 'G5', ddr: 'DDR5', cpu: 'GENOA', pcie: 'PCIe 5', networking: '2× 10GbE + OCP3.0 PCIe5', base: { price: 12000, weight: 43, id: '50492208611546' }, memory: [ { gb: 32, price: 0 }, { gb: 192, ...ddr5Upgrade }, ] }, ]; const storage = [ { name: 'None', price: 0, detail: 'Boot drive only' }, { name: '4 TB RAID', drives: '4x 1 TB drives', price: 2000, weight: 0.75, id: '67507609764058' }, { name: '16 TB RAID', drives: '4x 4 TB drives', price: 6000, weight: 0.75, id: '67507609796826' }, { name: '4x 4 TB RAID', drives: '16x 1 TB drives', description: '238.4 GB/s bandwidth · G5 only', platform: 'G5', usbBoot: true, price: 2000, weight: 0.75, quantity: 4, id: '67507609764058' }, ]; // GPU RAM (GB) and memory bandwidth (GB/s) are per card; multiply by quantity for aggregate totals. const r9700 = { price: 2500, weight: 2.5, id: '67533174571226', ram: 32, bandwidth: 640, vendor: 'amd', noise: '< 60 dB, GPU blowers are audible' }; // One R9700; quantities 4 or 6. const gpus = [ { name: 'None', price: 0, ram: 0, bandwidth: 0 }, { name: '4× AMD 9070XT', price: 1000, weight: 3, id: '67507611271386', ram: 16, bandwidth: 640, quantity: 4, vendor: 'amd' }, { name: '4× AMD R9700', ...r9700, quantity: 4 }, { name: '6× AMD R9700', ...r9700, quantity: 6, note: 'limited to 225W' }, ]; const presets = [ { name: 'tinybox base', platform: 0, memory: 0, storage: 0, gpu: 0 }, { name: 'tinybox red flash', vendor: 'amd', platform: 0, memory: 0, storage: 0, gpu: gpus.findIndex(gpu => gpu.quantity === 6) }, { name: 'tinybox red kimi', vendor: 'amd', platform: 1, memory: 0, storage: 3, gpu: 2, storageDescription: 'ultra fast 16TB RAID' }, ]; const shop = 'https://tinycorp.myshopify.com'; const money = value => '$' + value.toLocaleString('en-US'); const cost = item => item.price * (item.quantity || 1); const weight = items => Math.round(items.reduce((sum, item) => sum + (item.weight || 0) * (item.quantity || 1), 0)); const form = document.querySelector('#configurator'); const checkout = document.querySelector('#checkout'); const backToModels = document.querySelector('#back-to-models'); function choices(name, options, selectedIndex = 0) { return options.map((option, index) => ` `).join(''); } function fieldset(name, title, step) { return `
${step ? `0${step}` : ''}${title}
`; } document.querySelector('#preset-options').innerHTML = fieldset('preset', 'Choose your tinybox'); document.querySelector('#preset-choices').innerHTML = choices('preset', presets.map(preset => { const platform = platforms[preset.platform]; const memory = platform.memory[preset.memory]; const extra = storage[preset.storage]; const gpu = gpus[preset.gpu]; return { label: `${preset.name.replace(/\bred\b/, 'red')}`, vendor: preset.vendor, description: `${platform.name} · ${memory.gb} GB ${platform.ddr}${extra.price ? ` · ${preset.storageDescription || extra.name}` : ''}
${gpu.quantity ? `${gpu.name}
${gpu.ram * gpu.quantity} GB GPU RAM · ${(gpu.bandwidth * gpu.quantity).toLocaleString('en-US')} GB/s` : 'Bring your own GPUs · Fits up to 4'}`, detail: money([platform.base, memory, extra, gpu].reduce((sum, item) => sum + cost(item), 0)), }; })) + ` `; document.querySelector('#options').innerHTML = [ fieldset('platform', 'Platform', 1), fieldset('memory', 'Memory', 2), fieldset('storage', 'Extra storage', 3), fieldset('gpu', 'GPUs', 4), ].join(''); document.querySelector('#platform-choices').innerHTML = choices('platform', platforms.map(platform => ({ label: platform.name, detail: `${platform.ddr}
${platform.cpu}
${platform.pcie}
${money(platform.base.price)} base`, }))); for (const [name, options] of [['storage', storage], ['gpu', gpus]]) { document.querySelector(`#${name}-choices`).innerHTML = choices(name, options.map(option => ({ label: option.name, description: option.description, vendor: option.vendor, detail: option.price ? '+' + money(cost(option)) : (option.detail || 'No extra cost'), }))); } const selected = name => Number(form.elements.namedItem(name).value); function renderMemory(selectedIndex = 0) { const platform = platforms[selected('platform')]; document.querySelector('#memory-choices').innerHTML = choices('memory', platform.memory.map(memory => ({ label: `${memory.gb} GB`, detail: memory.price ? '+' + money(cost(memory)) : 'Included', })), selectedIndex); } function supportsStorage(platform, extra) { return !extra.platform || extra.platform === platform.name; } function compatible(extra, gpu) { return (extra.quantity || 1) === 1 || gpu.quantity !== 6; } function renderAvailability() { const platform = platforms[selected('platform')]; const extra = storage[selected('storage')]; const gpu = gpus[selected('gpu')]; for (const input of form.querySelectorAll('input[name="storage"]')) { const option = storage[Number(input.value)]; input.disabled = !supportsStorage(platform, option) || !compatible(option, gpu); } for (const input of form.querySelectorAll('input[name="gpu"]')) { input.disabled = !compatible(extra, gpus[Number(input.value)]); } } function applyPreset(index) { const preset = presets[index]; form.elements.namedItem('platform').value = String(preset.platform); renderMemory(preset.memory); form.elements.namedItem('storage').value = String(preset.storage); form.elements.namedItem('gpu').value = String(preset.gpu); update(); renderGallery(); } function syncPreset() { const index = presets.findIndex(preset => ['platform', 'memory', 'storage', 'gpu'].every(name => preset[name] === selected(name))); for (const input of form.querySelectorAll('input[name="preset"]')) { input.checked = Number(input.value) === index; } } function selection() { const platform = platforms[selected('platform')]; const memory = platform.memory[selected('memory')]; const base = platform.base; const extra = storage[selected('storage')]; const gpu = gpus[selected('gpu')]; const valid = supportsStorage(platform, extra) && compatible(extra, gpu); const items = [base, memory, extra, gpu].filter(item => item.price > 0); return { platform, base, memory, extra, gpu, items, valid, ready: valid && base.price != null && Boolean(base.id) && items.every(item => item.id && !item.outOfStock) }; } function update() { renderAvailability(); syncPreset(); const { platform, base, memory, extra, gpu, items, valid, ready } = selection(); const priced = base.price != null; document.querySelector('#total').value = priced ? money(items.reduce((sum, item) => sum + cost(item), 0)) : '—'; document.querySelector('.currency').hidden = !priced; const preset = presets.find(preset => ['platform', 'memory', 'storage', 'gpu'].every(name => preset[name] === selected(name))); document.querySelector('#order-model').textContent = preset ? preset.name : 'Your tinybox'; document.querySelector('#lead-time').textContent = gpu.outOfStock ? 'Out of stock.' : 'Ships in 1–8 weeks.'; const specs = { platform: `${platform.name} / ${platform.pcie}`, cpu: `32 core ${platform.cpu}, water cooled`, memory: `${memory.gb} GB ${platform.ddr}`, storage: '1 TB boot SSD' + (extra.price ? ` + ${extra.drives}` : ''), networking: (extra.quantity === 4 ? platform.networking?.replace(/ \+ OCP3\.0.*$/, '') : platform.networking) || 'Not specified', gpu: gpu.name, 'gpu-ram': `${gpu.ram * (gpu.quantity || 1)} GB, ${(gpu.bandwidth * (gpu.quantity || 1)).toLocaleString('en-US')} GB/s`, power: '1700W PSU, 100V~240V', noise: gpu.noise || '< 50 dB, low speed Noctua fans', board: 'Server grade with BMC', slots: `Up to ${Math.max(4, gpu.quantity || 0)} GPUs, full-fabric PCIe`, size: `${weight([base, memory, extra, gpu])} lbs, 12″W × 19¼″H × 18″D`, }; for (const [name, value] of Object.entries(specs)) { document.querySelector(`#spec-${name}`).textContent = value; } if (extra.usbBoot || (extra.price > 0 && gpu.quantity === 6)) { const warning = document.createElement('span'); warning.className = 'storage-warning'; warning.textContent = 'USB'; document.querySelector('#spec-storage').replaceChildren('1 TB ', warning, ` boot SSD + ${extra.drives}`); } if (gpu.note) { const note = document.createElement('span'); note.className = 'spec-note'; note.textContent = ` (${gpu.note})`; document.querySelector('#spec-gpu').append(note); } if (gpu.noise) { const warning = document.createElement('span'); warning.className = 'noise-warning'; warning.textContent = gpu.noise.split(',')[0]; document.querySelector('#spec-noise').replaceChildren(warning, gpu.noise.slice(warning.textContent.length)); } checkout.disabled = !ready; const note = document.querySelector('#checkout-note'); note.hidden = ready; note.textContent = gpu.outOfStock ? 'Selected GPUs are out of stock.' : !supportsStorage(platform, extra) ? 'Selected storage requires G5.' : !valid ? '6× R9700 GPUs cannot be combined with 4x 4 TB RAID storage.' : 'Checkout not connected.'; } function renderMode(customizing) { const scrollTop = window.scrollY; document.querySelector('#options').hidden = !customizing; document.querySelector('#presets').hidden = customizing; backToModels.hidden = !customizing; // Keep browser scroll anchoring from moving the page when the form changes height. window.scrollTo(0, scrollTop); } function navigateMode(customizing) { if (!customizing) { const matching = presets.findIndex(preset => ['platform', 'memory', 'storage', 'gpu'].every(name => preset[name] === selected(name))); applyPreset(matching >= 0 ? matching : presets.findIndex(preset => preset.platform === selected('platform'))); } renderMode(customizing); document.querySelector(customizing ? '#platform-choices input:checked' : '#preset-choices input:checked') .focus({ preventScroll: true }); } window.history.scrollRestoration = 'manual'; backToModels.addEventListener('click', () => window.history.back()); document.querySelector('#customize-own').addEventListener('click', () => { window.history.pushState({ ...window.history.state, tinyboxMode: 'customize' }, ''); navigateMode(true); }); window.addEventListener('popstate', event => { navigateMode(event.state?.tinyboxMode === 'customize'); }); form.addEventListener('change', event => { if (event.target.name === 'preset') { applyPreset(selected('preset')); return; } if (event.target.name === 'platform') { if (!supportsStorage(platforms[selected('platform')], storage[selected('storage')])) { form.elements.namedItem('storage').value = '0'; } renderMemory(selected('memory')); renderGallery(); } update(); }); form.addEventListener('submit', event => { event.preventDefault(); const { items, ready } = selection(); if (ready) { const cart = items.map(item => `${item.id}:${item.quantity || 1}`).join(','); window.location.assign(`${shop}/cart/${cart}`); } }); let currentView = 'front'; const photos = document.querySelectorAll('.photo-stage img'); const viewButtons = document.querySelectorAll('.views button'); function renderGallery() { const platform = platforms[selected('platform')].name; const matches = item => !item.dataset.platform || item.dataset.platform === platform; if (![...viewButtons].some(button => matches(button) && button.dataset.view === currentView)) { currentView = 'front'; } for (const photo of photos) { photo.hidden = !matches(photo) || photo.dataset.view !== currentView; } for (const button of viewButtons) { button.hidden = !matches(button); const active = !button.hidden && button.dataset.view === currentView; button.setAttribute('aria-pressed', String(active)); if (active) document.querySelector('#photo-caption').textContent = button.getAttribute('aria-label'); } } viewButtons.forEach(button => { button.addEventListener('click', () => { currentView = button.dataset.view; renderGallery(); }); }); applyPreset(presets.findIndex(preset => preset.name === 'tinybox base')); renderMode(false); form.hidden = false; if (window.history.state?.tinyboxMode === 'customize') { // Refresh returns to the existing models entry, rather than creating a duplicate. window.history.back(); } else { window.history.replaceState({ ...window.history.state, tinyboxMode: 'models' }, ''); }