Fenvynvyn Studios
Catalog
About
Favorites
Cart
FAQ
Contact
Theme
Sign in
Book
Menu
Catalog
About
Favorites
Cart
FAQ
Contact
Theme
Sign in
Sign in
Email
Password
Sign in
No account?
Register
Close
Create account
Email
Password
Register
Close
Province for tax estimate
Select province/territory
Alberta (AB)
British Columbia (BC)
Manitoba (MB)
New Brunswick (NB)
Newfoundland and Labrador (NL)
Nova Scotia (NS)
Ontario (ON)
Prince Edward Island (PE)
Quebec (QC)
Saskatchewan (SK)
Northwest Territories (NT)
Nunavut (NU)
Yukon (YT)
Subtotal
$0.00
Total
$0.00
Clear cart
Proceed to booking
Taxes are estimated based on selected province and finalized at payment.
Fenvynvyn Studios
Catalog
About
Favorites
Cart
FAQ
Contact
Theme
Sign in
Book
Menu
Catalog
About
Favorites
Cart
FAQ
Contact
Theme
Sign in
Sign in
Email
Password
Sign in
No account?
Register
Close
Create account
Email
Password
Register
Close
We will send a confirmation and payment link to your email.
I agree to the booking policy and 24h reschedule terms.
Place order
Fenvynvyn Studios
Catalog
About
Favorites
Cart
FAQ
Contact
Theme
Sign in
Book
Menu
Catalog
About
Favorites
Cart
FAQ
Contact
Theme
Sign in
Sign in
Email
Password
Sign in
No account?
Register
Close
Create account
Email
Password
Register
Close
OK
Fenvynvyn Studios
Catalog
About
Favorites
Cart
FAQ
Contact
Theme
Sign in
Book
Menu
Catalog
About
Favorites
Cart
FAQ
Contact
Theme
Sign in
Sign in
Email
Password
Sign in
No account?
Register
Close
Create account
Email
Password
Register
Close
We use necessary cookies to keep your cart and session. You can accept to enable analytics cookies. See details in the footer.
Accept all
Necessary only
Fenvynvyn Studios
Catalog
About
Favorites
Cart
FAQ
Contact
Theme
Sign in
Book
Menu
Catalog
About
Favorites
Cart
FAQ
Contact
Theme
Sign in
Sign in
Email
Password
Sign in
No account?
Register
Close
Create account
Email
Password
Register
Close
Your hold time has expired. Update your cart and proceed again.
OK
`; 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
How to add studios?
`; 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
Catalog
About
Favorites
Cart
FAQ
Contact
Theme
Sign in
Book
Menu
Catalog
About
Favorites
Cart
FAQ
Contact
Theme
Sign in
Sign in
Email
Password
Sign in
No account?
Register
Close
Create account
Email
Password
Register
Close
Use the catalog to select a studio and choose desired hours. Your selections will appear here.
Got it
`; 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))}
Remove
`; 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 = `
Thank you,
${chk.payload.name}
!
Your order
${orderId}
has been created for a total of
${currency.format(total)}
.
We sent a receipt to
${chk.payload.email}
.
`; $(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
Catalog
About
Favorites
Cart
FAQ
Contact
Theme
Sign in
Book
Menu
Catalog
About
Favorites
Cart
FAQ
Contact
Theme
Sign in
Sign in
Email
Password
Sign in
No account?
Register
Close
Create account
Email
Password
Register
Close
Please select a province/territory for tax estimation before proceeding.
OK
`; 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(); })();