Fenvynvyn Studios logo Fenvynvyn Studios

Sign in

No account?

Create account

Sign in

No account?

Create account

Sign in

No account?

Create account

Sign in

No account?

Create account

Fenvynvyn Studios logo Fenvynvyn Studios

Sign in

No account?

Create account

`; document.body.appendChild(d); const closeBtns = d.querySelectorAll('button'); closeBtns.forEach(b=> b.addEventListener('click', ()=> d.close())); d.addEventListener('close', ()=> { d.remove(); }); d.showModal(); } } } else { checkoutBtn.disabled = cart.length === 0; el.classList.remove('text-red-600','dark:text-red-400'); } } tick(); holdInterval = setInterval(tick, 1000); } function stopHoldTimer(){ if (holdInterval) clearInterval(holdInterval); holdInterval = null; } function cartSubtotal(){ return cart.reduce((sum,item)=>{ const info = catalog[item.id] || {}; const price = Number(info.pricePerHour || info.price || 120); return sum + price * Number(item.hours || 0); },0); } function calcTaxes(subtotal, prov){ if (!prov || !TAX[prov]) return { total:0, lines:[] }; const taxes = TAX[prov]; const lines = Object.entries(taxes).map(([name, rate])=>{ const amount = subtotal * rate; return { name, rate, amount }; }); const total = lines.reduce((s,l)=> s + l.amount, 0); return { total, lines }; } function renderCart(){ const list = $(ids.cartList); list.innerHTML = ''; if (cart.length === 0){ list.innerHTML = `
  • Your cart is empty
  • `; const hintBtn = $('#open-hint'); if (hintBtn){ hintBtn.addEventListener('click', ()=>{ const d = document.createElement('dialog'); d.className = 'rounded-2xl w-[95%] max-w-xl border border-slate-200 dark:border-slate-800 p-0 overflow-hidden'; d.innerHTML = `
    Fenvynvyn Studios logo Fenvynvyn Studios

    Sign in

    No account?

    Create account

    `; document.body.appendChild(d); d.querySelectorAll('button').forEach(b=> b.addEventListener('click', ()=> d.close())); d.addEventListener('close', ()=> d.remove()); d.showModal(); }); } updateSummary(); $(ids.checkout).disabled = true; clearHold(); startHoldTimer(); return; } const frag = document.createDocumentFragment(); cart.forEach(item=>{ const info = catalog[item.id] || {}; const title = info.title || info.name || `Studio ${item.id}`; const price = Number(info.pricePerHour || info.price || 120); const line = document.createElement('li'); line.className = 'grid grid-cols-[auto,1fr,auto] gap-4 items-center border border-slate-200 dark:border-slate-800 rounded-xl p-4 bg-white dark:bg-slate-900'; line.setAttribute('data-id', String(item.id)); const preview = document.createElement('img'); preview.src = './images/ultra_futuristic_recording_studio_with_neon_acoustic_panels_mixing_console_closeup_high_detail_photorealistic_wide_angle_softbox_lighting.jpg'; preview.alt = title + ' preview image'; preview.className = 'h-16 w-24 rounded-md object-cover border border-slate-200 dark:border-slate-800'; const infoBox = document.createElement('ul'); infoBox.className = 'grid gap-1'; infoBox.innerHTML = `
  • ${title}
  • ${currency.format(price)}/hour
  • `; const controls = document.createElement('ul'); controls.className = 'grid gap-2'; controls.innerHTML = `
  • ${currency.format(price * (Number(item.hours)||1))}
  • `; line.appendChild(preview); line.appendChild(infoBox); line.appendChild(controls); frag.appendChild(line); }); list.appendChild(frag); attachItemHandlers(); updateSummary(); ensureHoldStart(); startHoldTimer(); } function attachItemHandlers(){ $$(ids.cartList + ' li').forEach(li=>{ const id = li.getAttribute('data-id'); const input = li.querySelector('input.qty'); const btnMinus = li.querySelector('button.minus'); const btnPlus = li.querySelector('button.plus'); const btnRemove = li.querySelector('button.remove'); const lineTotalEl = li.querySelector('.line-total'); function updateLine(){ let qty = parseInt(input.value || '1', 10); if (isNaN(qty) || qty < 1) qty = 1; if (qty > 24) qty = 24; input.value = String(qty); const info = catalog[id] || {}; const price = Number(info.pricePerHour || info.price || 120); lineTotalEl.textContent = currency.format(price * qty); const target = cart.find(x=> String(x.id) === String(id)); if (target) target.hours = qty; saveCart(); updateSummary(); } btnMinus.addEventListener('click', ()=>{ input.value = String(Math.max(1, parseInt(input.value||'1',10) - 1)); updateLine(); }); btnPlus.addEventListener('click', ()=>{ input.value = String(Math.min(24, parseInt(input.value||'1',10) + 1)); updateLine(); }); input.addEventListener('change', updateLine); input.addEventListener('input', ()=> { // live restrict if (input.value.length > 2) input.value = input.value.slice(0,2); }); btnRemove.addEventListener('click', ()=>{ cart = cart.filter(x=> String(x.id) !== String(id)); saveCart(); if (cart.length === 0){ clearHold(); } renderCart(); }); }); } function updateSummary(){ const subtotal = cartSubtotal(); $(ids.subtotal).textContent = currency.format(subtotal); const taxBox = $(ids.taxLines); taxBox.innerHTML = ''; const { total:taxTotal, lines } = calcTaxes(subtotal, province); if (lines.length){ lines.forEach(l=>{ const li = document.createElement('li'); li.className = 'flex items-center justify-between text-sm text-slate-700 dark:text-slate-300'; li.innerHTML = `${l.name} (${(l.rate*100).toFixed(2).replace(/\.00$/,'')}%)${currency.format(l.amount)}`; taxBox.appendChild(li); }); } else { const li = document.createElement('li'); li.className = 'text-sm text-slate-500 dark:text-slate-400'; li.textContent = 'Select a province to estimate taxes'; taxBox.appendChild(li); } $(ids.total).textContent = currency.format(subtotal + taxTotal); const checkoutBtn = $(ids.checkout); checkoutBtn.disabled = cart.length === 0 || holdRemaining() <= 0; } function openConfirm(){ const d = $(ids.confirmDialog); const list = $(ids.confirmItems); list.innerHTML = ''; const frag = document.createDocumentFragment(); cart.forEach(item=>{ const info = catalog[item.id] || {}; const title = info.title || info.name || `Studio ${item.id}`; const price = Number(info.pricePerHour || info.price || 120); const li = document.createElement('li'); li.className = 'flex items-center justify-between text-sm'; li.innerHTML = `${title} × ${Number(item.hours||1)}h${currency.format(price * Number(item.hours||1))}`; frag.appendChild(li); }); const subtotal = cartSubtotal(); const taxes = calcTaxes(subtotal, province); const br1 = document.createElement('li'); br1.className = 'border-t border-slate-200 dark:border-slate-800 my-2'; frag.appendChild(br1); const sLi = document.createElement('li'); sLi.className = 'flex items-center justify-between text-sm'; sLi.innerHTML = `Subtotal${currency.format(subtotal)}`; frag.appendChild(sLi); taxes.lines.forEach(l=>{ const tLi = document.createElement('li'); tLi.className = 'flex items-center justify-between text-sm'; tLi.innerHTML = `${l.name} ${(l.rate*100).toFixed(2).replace(/\.00$/,'')}%${currency.format(l.amount)}`; frag.appendChild(tLi); }); const tLi2 = document.createElement('li'); tLi2.className = 'flex items-center justify-between text-base'; tLi2.innerHTML = `Total${currency.format(subtotal + taxes.total)}`; frag.appendChild(tLi2); list.appendChild(frag); $(ids.confirmErrors).textContent = ''; $(ids.placeOrder).disabled = false; d.showModal(); } function validateForm(){ const name = $(ids.custName).value.trim(); const email = $(ids.custEmail).value.trim(); const phone = $(ids.custPhone).value.trim(); const agree = $(ids.agree).checked; const errors = []; if (name.length < 2) errors.push('Please enter your full name.'); const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRe.test(email)) errors.push('Please enter a valid email address.'); const phoneRe = /^\+?1?\s*\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{4}$/; if (!phoneRe.test(phone)) errors.push('Please enter a valid Canadian/US phone number.'); if (!agree) errors.push('You must agree to the booking policy.'); return { valid: errors.length === 0, errors, payload: {name,email,phone} }; } function placeOrder(){ const chk = validateForm(); const errBox = $(ids.confirmErrors); if (!chk.valid){ errBox.innerHTML = chk.errors.map(e=> `
  • • ${e}
  • `).join(''); return; } $(ids.placeOrder).disabled = true; setTimeout(()=>{ const orderId = 'FNV-' + Math.random().toString(36).slice(2,6).toUpperCase() + '-' + Math.random().toString(36).slice(2,6).toUpperCase(); const subtotal = cartSubtotal(); const taxes = calcTaxes(subtotal, province); const total = subtotal + taxes.total; $(ids.confirmDialog).close(); $(ids.resultMessage).innerHTML = ` `; $(ids.resultDialog).showModal(); // Clear cart and hold cart = []; saveCart(); clearHold(); localStorage.removeItem('holdExpiredNotified'); renderCart(); }, 600); } async function loadComponents(){ // header try { const res = await fetch('./header.html', {cache:'no-store'}); if (res.ok) { const html = await res.text(); $(ids.header).innerHTML = html; // Ensure theme button exists or add if (!document.getElementById('theme-toggle')) { const btn = document.createElement('button'); btn.id = 'theme-toggle'; btn.className = 'rounded-full border border-slate-200 dark:border-slate-700 p-2 hover:bg-slate-100 dark:hover:bg-slate-800 transition'; btn.innerHTML = '🌙'; $(ids.header).appendChild(btn); } } } catch(e){} // footer try { const res = await fetch('./footer.html', {cache:'no-store'}); if (res.ok) { const html = await res.text(); $(ids.footer).innerHTML = html; } } catch(e){} } async function loadCatalog(){ try { const res = await fetch('./catalog.json', {cache:'no-store'}); if (!res.ok) throw new Error('catalog'); const data = await res.json(); // Expect array or object if (Array.isArray(data)) { data.forEach(it=>{ const id = String(it.id ?? it.slug ?? it.uid ?? it.title ?? Math.random().toString(36).slice(2)); catalog[id] = it; }); } else { Object.keys(data).forEach(id=>{ catalog[String(id)] = data[id]; }); } } catch(e){ catalog = {}; } } function applyThemeButton(){ const btn = $(ids.themeToggle); const icon = $(ids.themeIcon) || (btn ? btn.querySelector('#theme-icon') : null); function setIcon(){ const dark = document.documentElement.classList.contains('dark'); if (icon) icon.textContent = dark ? '🌞' : '🌙'; } if (btn) { btn.addEventListener('click', ()=>{ const root = document.documentElement; const dark = root.classList.toggle('dark'); try { localStorage.setItem('theme', dark ? 'dark':'light'); } catch(e){} setIcon(); }); setIcon(); } } function initCookie(){ const accepted = localStorage.getItem('cookieConsent'); const d = $(ids.cookieDialog); if (!accepted) { setTimeout(()=> d.showModal(), 1000); } const accept = $(ids.cookieAccept); const decline = $(ids.cookieDecline); const close = $(ids.cookieClose); function set(consent){ localStorage.setItem('cookieConsent', consent); d.close(); } if (accept) accept.addEventListener('click', ()=> set('all')); if (decline) decline.addEventListener('click', ()=> set('necessary')); if (close) close.addEventListener('click', ()=> set('necessary')); } function bindCore(){ $(ids.province).addEventListener('change', (e)=>{ saveProvince(e.target.value); updateSummary(); }); $(ids.clear).addEventListener('click', ()=>{ cart = []; saveCart(); clearHold(); localStorage.removeItem('holdExpiredNotified'); renderCart(); }); $(ids.checkout).addEventListener('click', ()=>{ if (cart.length === 0) return; if (holdRemaining() <= 0) return; if (!province) { const d = document.createElement('dialog'); d.className = 'rounded-2xl w-[95%] max-w-lg border border-slate-200 dark:border-slate-800 p-0 overflow-hidden'; d.innerHTML = `
    Fenvynvyn Studios logo Fenvynvyn Studios

    Sign in

    No account?

    Create account

    `; document.body.appendChild(d); d.querySelectorAll('button').forEach(b=> b.addEventListener('click', ()=> d.close())); d.addEventListener('close', ()=> d.remove()); d.showModal(); return; } openConfirm(); }); $(ids.confirmClose).addEventListener('click', ()=> $(ids.confirmDialog).close()); $(ids.placeOrder).addEventListener('click', placeOrder); $(ids.resultClose).addEventListener('click', ()=> $(ids.resultDialog).close()); $(ids.resultOk).addEventListener('click', ()=> $(ids.resultDialog).close()); $(ids.resultDialog).addEventListener('close', ()=> { /* stay */ }); const yearEl = $(ids.yearNow); if (yearEl) yearEl.textContent = String(new Date().getFullYear()); } async function init(){ await loadComponents(); applyThemeButton(); await loadCatalog(); loadCart(); ensureHoldStart(); startHoldTimer(); bindCore(); loadProvince(); renderCart(); initCookie(); // Accessibility: close dialogs with Escape [$ (ids.confirmDialog), $(ids.resultDialog), $(ids.cookieDialog)].forEach(d=>{ if (!d) return; d.addEventListener('cancel', (e)=> { e.preventDefault(); d.close(); }); }); } init(); })();