Minimalist Photo Studio Rental
Book clean, light-filled studios by the hour. No hidden fees. Fully equipped, distraction-free spaces for creators.
High-contrast, accessible, and SEO-optimized. luminasip.click
Why creators pick LuminaSip
Transparent pricing
Clear hourly rates. Instant totals in your cart.
Natural light
Consistent daylight planning with availability previews.
Essential amenities
Seamless backdrops, Wi‑Fi, changing area, props shelf.
Instant booking
Reserve in minutes. Manage favorites and compare.
';
initHeaderInteractions();
initFooterInteractions();
}
})();
async function loadFeatured() {
const wrap = document.getElementById('featured-teaser');
if(!wrap) return;
const skeleton = () => {
wrap.innerHTML = Array.from({length:3}).map(()=>`
`).join('');
};
skeleton();
try {
const res = await fetch('catalog.json', {cache:'no-store'});
if(!res.ok) throw new Error('catalog.json not available');
const data = await res.json();
const safe = Array.isArray(data) ? data : [];
const top = safe.slice(0,3);
wrap.innerHTML = top.map(item => {
const name = String(item.name || 'Studio');
const short = String(item.short || 'Minimal, flexible studio built for clean sets.');
const capacity = String(item.capacity ?? '—');
const sizeSqm = String(item.sizeSqm ?? '—');
const pricePerHour = Number(item.pricePerHour ?? 0);
const id = Number(item.id ?? 0);
return `
${escapeHtml(name)}
${escapeHtml(short)}
Capacity: ${escapeHtml(capacity)} · ${escapeHtml(sizeSqm)} m² · $${Number.isFinite(pricePerHour)?pricePerHour.toFixed(0):'0'}/h
`;
}).join('');
wrap.querySelectorAll('.js-open-details').forEach(b=>{
b.addEventListener('click', async e=>{
const id = parseInt(e.currentTarget.dataset.id,10);
const item = safe.find(x=>Number(x.id)===id);
if (item && typeof window.openStudioDetails === 'function') window.openStudioDetails(item);
});
});
wrap.querySelectorAll('.js-add-fav').forEach(b=>{
b.addEventListener('click', e=>{
const id = parseInt(e.currentTarget.dataset.id,10);
const fav = safeJsonArray(localStorage.getItem('psr_favorites'));
if (!fav.includes(id)) fav.push(id);
localStorage.setItem('psr_favorites', JSON.stringify(fav));
if (typeof window.toast === 'function') window.toast('Added to favorites');
});
});
if(top.length===0){
wrap.innerHTML = `
Featured studios will appear here once the catalog is available.
`;
}
} catch(e){
console.error(e);
wrap.innerHTML = `
Featured studios
We couldn’t load the catalog right now. Try again later or browse the full list.
`;
}
}
document.addEventListener('DOMContentLoaded', loadFeatured);
function initHeaderInteractions(){
const root = document.documentElement;
const saved = localStorage.getItem('psr_theme') || 'light';
if (saved==='dark') root.classList.add('dark'); else root.classList.remove('dark');
document.querySelectorAll('[data-open-modal]').forEach(btn=>{
btn.addEventListener('click', ()=>{
const id = btn.getAttribute('data-open-modal');
const m = document.getElementById(id);
if(m){
m.classList.remove('hidden');
m.setAttribute('aria-hidden','false');
const focusable = m.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (focusable) focusable.focus();
}
});
});
document.querySelectorAll('[data-close-modal]').forEach(btn=>{
btn.addEventListener('click', ()=>{
const id = btn.getAttribute('data-close-modal');
const m = document.getElementById(id);
if(m){
m.classList.add('hidden');
m.setAttribute('aria-hidden','true');
}
});
});
document.addEventListener('keydown', (e)=>{
if(e.key !== 'Escape') return;
const open = document.querySelector('[role="dialog"]:not(.hidden), dialog[open]');
if(open){
open.classList.add('hidden');
open.setAttribute('aria-hidden','true');
if (open.tagName === 'DIALOG' && typeof open.close === 'function') open.close();
}
});
window.openStudioDetails = function(item){
const m = document.getElementById('studio-details-modal');
if(!m) return;
const titleEl = m.querySelector('[data-mdl-title]');
const bodyEl = m.querySelector('[data-mdl-body]');
const addEl = m.querySelector('[data-mdl-addcart]');
const name = String(item.name || 'Studio');
const description = String(item.description || item.short || 'Minimal studio with flexible setup.');
const capacity = String(item.capacity ?? '—');
const sizeSqm = String(item.sizeSqm ?? '—');
const amenities = Array.isArray(item.amenities) ? item.amenities.map(x=>String(x)) : [];
const availability = Array.isArray(item.availability) ? item.availability.map(x=>String(x)) : [];
const price = Number(item.pricePerHour ?? 0);
const rating = Number(item.rating ?? 0);
if(titleEl) titleEl.textContent = name;
if(bodyEl){
bodyEl.innerHTML = `
${escapeHtml(description)}
- Capacity: ${escapeHtml(capacity)}
- Size: ${escapeHtml(sizeSqm)} m²
- Amenities: ${escapeHtml(amenities.length?amenities.join(', '):'—')}
- Availability: ${escapeHtml(availability.length?availability.join(' · '):'—')}
$${Number.isFinite(price)?price.toFixed(0):'0'}/hour · Rating ${Number.isFinite(rating)?rating.toFixed(1):'0.0'}
`;
}
if(addEl) addEl.setAttribute('data-id', String(item.id ?? ''));
m.classList.remove('hidden');
m.setAttribute('aria-hidden','false');
const first = m.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if(first) first.focus();
};
window.toast = function(msg){
const t = document.getElementById('toast');
if(!t) return;
t.textContent = String(msg || '');
t.classList.remove('hidden');
t.setAttribute('aria-hidden','false');
clearTimeout(window.__psrToastTimer);
window.__psrToastTimer = setTimeout(()=>{
t.classList.add('hidden');
t.setAttribute('aria-hidden','true');
}, 1600);
};
document.body.addEventListener('click', e=>{
const add = e.target && e.target.closest && e.target.closest('[data-mdl-addcart]');
if(add){
const id = parseInt(add.getAttribute('data-id'),10);
if(!Number.isFinite(id)) return;
const cart = safeJsonArray(localStorage.getItem('psr_cart')).map(x=>{
if(x && typeof x === 'object') return x;
return null;
}).filter(Boolean);
const found = cart.find(i=>Number(i.id)===id);
if(found) found.qty = Number(found.qty || 1) + 1;
else cart.push({id:id, qty:1});
localStorage.setItem('psr_cart', JSON.stringify(cart));
if (typeof window.toast === 'function') window.toast('Added to cart');
return;
}
const modalBackdrop = e.target && e.target.matches && e.target.matches('[role="dialog"]');
if(modalBackdrop){
const closeBtn = modalBackdrop.querySelector('[data-close-modal]');
if(closeBtn) closeBtn.click();
}
});
const newsletterForm = document.getElementById('newsletter-form');
const newsletterEmail = document.getElementById('newsletter-email');
const newsletterConsent = document.getElementById('newsletter-consent');
const newsletterHint = document.getElementById('newsletter-hint');
if(newsletterForm && newsletterEmail && newsletterConsent){
const setHint = (text, ok) => {
if(!newsletterHint) return;
newsletterHint.textContent = text;
newsletterHint.className = ok ? 'mt-2 text-xs text-gray-600' : 'mt-2 text-xs text-red-700';
};
const validate = () => {
const v = String(newsletterEmail.value || '').trim();
const okEmail = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i.test(v);
const okConsent = !!newsletterConsent.checked;
if(!okEmail){
newsletterEmail.setAttribute('aria-invalid','true');
setHint('Enter a valid email address (e.g., [email protected]).', false);
return false;
}
newsletterEmail.removeAttribute('aria-invalid');
if(!okConsent){
setHint('Please confirm consent to receive updates.', false);
return false;
}
setHint('We send no spam. Unsubscribe anytime.', true);
return true;
};
newsletterEmail.addEventListener('input', validate);
newsletterConsent.addEventListener('change', validate);
newsletterForm.addEventListener('submit', (e)=>{
if(!validate()){
e.preventDefault();
newsletterEmail.focus();
return;
}
try{
localStorage.setItem('psr_newsletter_email', String(newsletterEmail.value || '').trim());
localStorage.setItem('psr_newsletter_ts', String(Date.now()));
}catch(_){}
if (typeof window.toast === 'function') window.toast('Subscription ready');
});
}
}
function initFooterInteractions(){
const key='psr_cookie_ok';
const b = document.getElementById('cookie-banner');
if (b && localStorage.getItem(key)==='1') { b.classList.add('hidden'); b.setAttribute('aria-hidden','true'); }
document.querySelectorAll('[data-accept-cookies]').forEach(btn=>{
btn.addEventListener('click', ()=>{
localStorage.setItem(key,'1');
if(b){
b.classList.add('hidden');
b.setAttribute('aria-hidden','true');
}
if (typeof window.toast === 'function') window.toast('Cookie preferences saved');
});
});
document.querySelectorAll('[data-decline-cookies]').forEach(btn=>{
btn.addEventListener('click', ()=>{
localStorage.setItem(key,'0');
if(b){
b.classList.add('hidden');
b.setAttribute('aria-hidden','true');
}
if (typeof window.toast === 'function') window.toast('Non-essential cookies declined');
});
});
const off = document.getElementById('offline-note');
function sync(){ if(off) off.classList.toggle('hidden', navigator.onLine); }
window.addEventListener('online', sync);
window.addEventListener('offline', sync);
sync();
const year = document.getElementById('footer-year');
if(year) year.textContent = String(new Date().getFullYear());
const countdownEl = document.getElementById('next-drop-countdown');
if(countdownEl){
const now = new Date();
const target = new Date(now);
target.setHours(18,0,0,0);
if(target.getTime() <= now.getTime()) target.setDate(target.getDate()+1);
const tick = ()=>{
const t = target.getTime() - Date.now();
if(t <= 0){
countdownEl.textContent = "00:00:00";
return;
}
const s = Math.floor(t/1000);
const hh = String(Math.floor(s/3600)).padStart(2,'0');
const mm = String(Math.floor((s%3600)/60)).padStart(2,'0');
const ss = String(s%60).padStart(2,'0');
countdownEl.textContent = `${hh}:${mm}:${ss}`;
};
tick();
window.__psrCountdownTimer && clearInterval(window.__psrCountdownTimer);
window.__psrCountdownTimer = setInterval(tick, 1000);
}
}
function safeJsonArray(s){
try{
const v = JSON.parse(s || '[]');
return Array.isArray(v) ? v : [];
} catch(e){
return [];
}
}
function escapeHtml(str){
return String(str)
.replaceAll('&','&')
.replaceAll('<','<')
.replaceAll('>','>')
.replaceAll('"','"')
.replaceAll("'","'");
}