Create Your Own Combo
Choose a Beanie
Choose a Headband
Combo Builder
<div class="tw-combo">
<h2>Create Your Own Combo</h2>
<div class="tw-grid">
<div class="tw-col">
<h3>Choose a Beanie</h3>
<select id="twBeanieSelect">
<option value="">Loading…</option>
</select>
<div class="tw-preview">
<img id="twBeanieImg" alt="" />
<div id="twBeanieMeta"></div>
</div>
</div>
<div class="tw-col">
<h3>Choose a Headband</h3>
<select id="twHeadbandSelect">
<option value="">Loading…</option>
</select>
<div class="tw-preview">
<img id="twHeadbandImg" alt="" />
<div id="twHeadbandMeta"></div>
</div>
</div>
</div>
<div class="tw-actions">
<button id="twResetBtn" type="button">Reset</button>
<button id="twAddBtn" type="button" disabled>Add Combo to Cart</button>
<div id="twStatus" role="status" aria-live="polite"></div>
</div>
</div>
<style>
.tw-combo{max-width:1000px;margin:0 auto;padding:24px}
.tw-grid{display:grid;grid-template-columns:1fr;gap:24px}
@media(min-width:800px){.tw-grid{grid-template-columns:1fr 1fr}}
.tw-col select{width:100%;padding:12px;border:1px solid #ddd;border-radius:10px}
.tw-preview{margin-top:14px;border:1px solid #eee;border-radius:14px;padding:14px}
.tw-preview img{width:100%;height:auto;display:none;border-radius:12px}
.tw-actions{display:flex;gap:12px;align-items:center;margin-top:18px;flex-wrap:wrap}
.tw-actions button{padding:12px 16px;border-radius:12px;border:1px solid #111;background:#111;color:#fff;cursor:pointer}
.tw-actions button[disabled]{opacity:.5;cursor:not-allowed}
#twResetBtn{background:#fff;color:#111}
#twStatus{min-height:20px}
</style>
<script>
(async function () {
const COLLECTION_BEANIES = 'beanies';
const COLLECTION_HEADBANDS = 'headbands';
const $ = (id) => document.getElementById(id);
const beanieSelect = $('twBeanieSelect');
const headbandSelect = $('twHeadbandSelect');
const addBtn = $('twAddBtn');
const resetBtn = $('twResetBtn');
const statusEl = $('twStatus');
const beanieImg = $('twBeanieImg');
const headbandImg = $('twHeadbandImg');
const beanieMeta = $('twBeanieMeta');
const headbandMeta = $('twHeadbandMeta');
let beanies = [];
let headbands = [];
function money(cents) {
try { return (cents / 100).toLocaleString(undefined, { style: 'currency', currency: 'USD' }); }
catch { return `$${(cents/100).toFixed(2)}`; }
}
function setStatus(msg, isError=false) {
statusEl.textContent = msg || '';
statusEl.style.color = isError ? 'crimson' : '';
}
function firstAvailableVariant(p) {
// products.json gives variants[] with available + id
return (p.variants || []).find(v => v.available) || null;
}
function buildOptions(selectEl, products) {
selectEl.innerHTML = '<option value="">Select…</option>';
products.forEach(p => {
const v = firstAvailableVariant(p);
const opt = document.createElement('option');
opt.value = v ? String(v.id) : '';
opt.dataset.handle = p.handle;
opt.dataset.title = p.title;
opt.dataset.image = (p.images && p.images[0]) ? p.images[0].src : '';
opt.dataset.price = v ? String(v.price) : '';
opt.disabled = !v;
opt.textContent = v ? `${p.title} — ${money(v.price)}` : `${p.title} — Sold out`;
selectEl.appendChild(opt);
});
}
function renderPreview(selectEl, imgEl, metaEl) {
const opt = selectEl.selectedOptions[0];
if (!opt || !opt.value) {
imgEl.style.display = 'none';
imgEl.removeAttribute('src');
metaEl.textContent = '';
return;
}
const title = opt.dataset.title || '';
const img = opt.dataset.image || '';
const price = opt.dataset.price ? money(Number(opt.dataset.price)) : '';
if (img) {
imgEl.src = img;
imgEl.style.display = 'block';
} else {
imgEl.style.display = 'none';
imgEl.removeAttribute('src');
}
metaEl.textContent = price ? `${title} • ${price}` : title;
}
function updateAddState() {
const ok = !!beanieSelect.value && !!headbandSelect.value;
addBtn.disabled = !ok;
}
async function loadCollection(handle) {
// Shopify collections endpoint that returns products + variants.
const url = `/collections/${handle}/products.json?limit=250`;
const res = await fetch(url, { credentials: 'same-origin' });
if (!res.ok) throw new Error(`Failed to load /collections/${handle} (${res.status})`);
const data = await res.json();
return data.products || [];
}
async function addComboToCart(beanieVariantId, headbandVariantId) {
const res = await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({
items: [
{ id: Number(beanieVariantId), quantity: 1 },
{ id: Number(headbandVariantId), quantity: 1 }
]
})
});
if (!res.ok) {
const text = await res.text().catch(()=>'');
throw new Error(`Cart add failed (${res.status}). ${text}`);
}
return res.json();
}
// Init
try {
setStatus('Loading combos…');
beanieSelect.innerHTML = '<option value="">Loading…</option>';
headbandSelect.innerHTML = '<option value="">Loading…</option>';
[beanies, headbands] = await Promise.all([
loadCollection(COLLECTION_BEANIES),
loadCollection(COLLECTION_HEADBANDS)
]);
buildOptions(beanieSelect, beanies);
buildOptions(headbandSelect, headbands);
setStatus('');
} catch (e) {
console.error(e);
setStatus(`Combo Viewer error: ${e.message}`, true);
}
beanieSelect.addEventListener('change', () => {
renderPreview(beanieSelect, beanieImg, beanieMeta);
updateAddState();
});
headbandSelect.addEventListener('change', () => {
renderPreview(headbandSelect, headbandImg, headbandMeta);
updateAddState();
});
resetBtn.addEventListener('click', () => {
beanieSelect.value = '';
headbandSelect.value = '';
renderPreview(beanieSelect, beanieImg, beanieMeta);
renderPreview(headbandSelect, headbandImg, headbandMeta);
updateAddState();
setStatus('');
});
addBtn.addEventListener('click', async () => {
try {
addBtn.disabled = true;
setStatus('Adding combo to cart…');
await addComboToCart(beanieSelect.value, headbandSelect.value);
setStatus('Added. Redirecting to cart…');
window.location.href = '/cart';
} catch (e) {
console.error(e);
setStatus(e.message || 'Failed to add combo.', true);
updateAddState();
}
});
// initial previews
renderPreview(beanieSelect, beanieImg, beanieMeta);
renderPreview(headbandSelect, headbandImg, headbandMeta);
updateAddState();
})();
</script>