// Receipt comparison, features, how it works, pricing, FAQ, final CTA, footer.
function ChipSvg({ type }) {
const s = 28, r = 6;
const chips = {
netflix: (
<>
>
),
viaplay: (
<>
via
>
),
disney: (
<>
D+
>
),
max: (
<>
MAX
>
),
tv4: (
<>
TV4
>
),
};
const key = /netflix/i.test(type) ? 'netflix'
: /viaplay/i.test(type) ? 'viaplay'
: /disney/i.test(type) ? 'disney'
: /hbo|max/i.test(type) ? 'max'
: /tv4/i.test(type) ? 'tv4'
: 'netflix';
return (
);
}
function BarcodeSvg() {
const pat = [2,1,2,1,3,2,1,2,2,1,3,1,2,2,1,3,2,1,1,2,3,1,2,1,1,3,2,1,3,1,1,2,2,3,1,2,1,1,3,2,1,2,3,1,2,1,1,2,3,2];
let x = 8;
const bars = [];
pat.forEach((w, i) => {
const bw = w * 2.6;
if (i % 2 === 0) bars.push();
x += bw;
});
return (
);
}
function ReceiptBad({ mini = false }) {
const C = COPY[window.locale];
const fmtPrice = p => p.replace(/\s*kr$/, ',00');
return (
{/* Handwritten annotations (full version only) */}
{!mini && (
{C.rbtAnnot1}
{C.rbtAnnot2}
{C.rbtAnnot3}
)}
{C.rbtCustomer}
15.05.26 14:32
{C.receiptItems.map(([name, price]) => (
))}
{C.rbtBillLabel}
· · · · · · ·
{C.rbtMonthLabel}
eller
8 580
kr
{C.rbtYearLabel}
{!mini && (
<>
{C.rbtMoms}
143,00
Kort • • • • 4471
{C.rbtApproved}
{C.rbtThanks}
>
)}
);
}
function ReceiptGood() {
const C = COPY[window.locale];
const monthlyPrice = 58;
const annualPrice = 699;
return (
TVMOMENTO
{monthlyPrice} {C.perMnd}
vid 1-årsplan
{C.goodFeatures.map(f => (
-
{f}
))}
{C.cta}
);
}
function ReceiptCompare() {
return null;
}
function FeatureSavings() {
const C = COPY[window.locale];
return (
{C.compareEyebrow}
{C.compareSaveH1}
{C.compareSaveH2}
{C.compareSaveBody}
| {C.compareRows[0][0]} | {C.compareRows[0][1]} | {C.compareRows[0][2]} |
| {C.compareRows[1][0]} | {C.compareRows[1][1]} | {C.compareRows[1][2]} |
| {C.compareRows[2][0]} | {C.compareRows[2][1]} | {C.compareRows[2][2]} |
);
}
function HowItWorks() {
const C = COPY[window.locale];
const hwPrices = { m3: 349, y1: 699, y3: 1299 };
React.useEffect(() => {
const io = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) { e.target.classList.add('visible'); io.unobserve(e.target); }
});
}, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
document.querySelectorAll('.how-step').forEach(s => io.observe(s));
return () => io.disconnect();
}, []);
return (
{C.howEyebrow}
{C.howH1}
{C.howH2}
{/* Step 01 — plan comparison */}
01
{C.howS1Verb}
{C.howS1Body}
{C.howPlans[0]}
{hwPrices.m3}
kr
{C.howPlans[1]}
{hwPrices.y1}
kr
{C.howPopular}
{C.howPlans[2]}
{hwPrices.y3}
kr
{/* Step 02 — payment sheet */}
02
{C.howS2VerbA} {C.howS2VerbB}
{C.howS2Body}
Tvmomento{C.howPlans[1]}
{C.howPayLabel}{hwPrices.y1} kr
{C.howPayTotal}{hwPrices.y1} kr
{/* Step 03 — cinematic image */}
03
{C.howS3VerbA} {C.howS3VerbB}
{C.howS3Body}
);
}
const DEVICE_IMAGES = ['assets/devices/tv.webp', 'assets/devices/phone.webp', 'assets/devices/tablet.webp', 'assets/devices/laptop.webp'];
const DEVICE_ALT = ['Smart TV', 'Phone', 'Tablet', 'Laptop'];
const DEVICE_LOGOS = [
/* TV */
,
/* Mobile */
,
/* Tablet */
,
/* Laptop */
,
];
function DeviceCompat() {
const C = COPY[window.locale];
const headerRef = React.useRef(null);
React.useEffect(() => {
const el = headerRef.current;
if (!el) return;
const io = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) { el.classList.add('visible'); io.disconnect(); }
}, { threshold: 0.05 });
io.observe(el);
return () => io.disconnect();
}, []);
return (
{C.deviceEyebrow}
{C.deviceHeadline}
{C.deviceSub}
{C.deviceIntro}
{C.devices.map((d, i) => (
{C.devicesLogosLabel[i]}
{DEVICE_LOGOS[i]}
))}
);
}
const CHECKOUT_WEBHOOK_URL = (typeof window !== 'undefined' && window.CHECKOUT_WEBHOOK_URL) ? window.CHECKOUT_WEBHOOK_URL : 'https://script.google.com/macros/s/AKfycbx_h_RdOCwuWzqOOf9P3I3g_1en18nHc8Bz81urBZS6S8Sc6rHfloSpYirtfdT7jQDS/exec';
const PRICING_COPY = {
SE: {
planNames: { '3m': '3 Månader', '1y': '1 År', '3y': '3 År + 6 mån gratis' },
planSub: { '3m': 'Engångsbelopp', '1y': 'Spara vs månadsvis', '3y': 'Bästa värdet — 3,5 år totalt' },
popularBadge: 'Populärast ★',
chipUnit1: 'skärm', chipUnitN: 'skärmar', chipIncluded: 'Ingår',
sumExtra: n => '+' + n + ' skärm' + (n > 1 ? 'ar' : ''),
numLocale: 'sv-SE', currency: 'SEK', locale: 'SE', defaultCC: '+46',
mCtaStep1: 'Välj antal skärmar →', mCtaStep2: 'Fortsätt →', mCtaStep3: 'Slutför beställning →', mCtaNote: 'Engångsbetalning',
scrollUpBtn: 'Välj plan ↑',
trustItems: ['Vi ringer inom 15 min', 'Inga kortuppgifter krävs', 'Krypterad anslutning (TLS 1.3)'],
panelLabel: 'Välj ditt paket', summaryLabel: 'Din beställning',
sectionLabel: 'Abonnemang', changeBtn: 'Ändra ↩',
devicesTitle: 'Antal skärmar', devicesNote: 'Varje skärm kan titta på olika kanaler samtidigt.',
chipPop: 'Vanligast',
sumPlan: 'Paket', sumBase: 'Baspris', sumTotal: 'Totalt att betala',
sumNote: 'Engångsbetalning — ingen prenumeration',
orderNote: 'Engångsbetalning — ingen prenumeration',
viewersSuffix: ' ser på detta just nu', slotsSuffix: ' platser kvar',
formPhoneLabel: 'Telefonnummer', formEmailLabel: 'E-postadress',
formPhoneErr: 'Ange ett giltigt telefonnummer', formEmailErr: 'Ange en giltig e-postadress',
formPrivacy: 'Vi använder dina uppgifter för att skicka inloggningen. Inget spam.',
formError: 'Något gick fel. Försök igen.',
submitLabel: 'Slutför beställning',
successBadge: 'PRISET LÅST', successHeading: 'Du är inne.',
successSavingsHero: 'Du sparar 7 884 kr det här året.',
successFromBefore: '715 kr/mån', successFromAfter: '58 kr/mån',
successFromNote: 'Från 5 separata appar till 1.',
successCallLine: 'Vi hör av oss på {phone} inom 15 min.',
successHumanNote: 'Ingen bot — en riktig människa hjälper dig igång på 10 minuter.',
successWelcome: 'Välkommen till 2 400+ smarta streamers. Nu är du en av dem.',
successFootnote: 'Bekräftelse skickas till {email}.',
successBeforeLbl: 'Tidigare', successAfterLbl: 'Nu',
},
};
const PRICING_PLANS = {
SE: {
'3m': { base: 349, extra: 175 },
'1y': { base: 699, extra: 350 },
'3y': { base: 1299, extra: 650 },
},
};
function Pricing() {
const ref = React.useRef(null);
const loc = window.locale || 'SE';
const TC = PRICING_COPY[loc];
const TP = PRICING_PLANS[loc];
React.useEffect(() => {
const root = ref.current;
if (!root) return;
const C = PRICING_COPY[window.locale || 'SE'];
const PLANS = PRICING_PLANS[window.locale || 'SE'];
let selPlan = '1y', selExtra = 0, planTapped = false, step2Active = false, step3Active = false;
let emailRevealed = false;
let selectedCC = C.defaultCC;
function fmt(n) { return n.toLocaleString(C.numLocale); }
function isMob() { return window.matchMedia('(max-width: 860px)').matches; }
function updateSummary() {
const plan = PLANS[selPlan], extra = selExtra * plan.extra, total = plan.base + extra;
const qe = id => root.querySelector('#' + id);
if (qe('sum-plan-name')) qe('sum-plan-name').textContent = C.planNames[selPlan];
if (qe('sum-base-price')) qe('sum-base-price').textContent = fmt(plan.base) + ' kr';
if (qe('sum-total')) qe('sum-total').textContent = fmt(total);
const lfPrePrice = root.querySelector('#lf-pre-price');
if (lfPrePrice) lfPrePrice.textContent = fmt(total);
const lfPrice = root.querySelector('#lf-price');
if (lfPrice) lfPrice.textContent = fmt(total);
const extraRow = qe('sum-extra-row');
if (extraRow) {
if (selExtra > 0) {
extraRow.classList.add('visible');
root.querySelector('#sum-extra-label').textContent = C.sumExtra(selExtra);
root.querySelector('#sum-extra-price').textContent = '+' + fmt(extra) + ' kr';
} else { extraRow.classList.remove('visible'); }
}
if (qe('ps-name')) qe('ps-name').textContent = C.planNames[selPlan];
if (qe('ps-price')) qe('ps-price').textContent = fmt(plan.base) + ' kr';
updateTopBar();
}
function updateChipPrices() {
const ppu = PLANS[selPlan].extra;
for (let i = 1; i <= 5; i++) {
const el = root.querySelector('#chip-price-' + i);
if (el) el.textContent = '+' + fmt(i * ppu) + ' kr';
}
}
const mCtaEl = root.querySelector('.m-cta');
const mCtaBtn = root.querySelector('.m-cta-btn');
const mTopBar = root.querySelector('#m-top-bar');
const checkoutPanel = root.querySelector('.checkout-grid .co-panel');
function syncCheckoutCtaState() {
const active = !!(mCtaEl && isMob() && mCtaEl.classList.contains('m-cta-up'));
document.body.classList.toggle('checkout-cta-active', active);
document.documentElement.classList.toggle('checkout-cta-active', active);
}
function keepCheckoutInView() {
if (!isMob() || !checkoutPanel) return;
const rect = checkoutPanel.getBoundingClientRect();
const ctaH = mCtaEl && mCtaEl.classList.contains('m-cta-up') ? mCtaEl.getBoundingClientRect().height : 0;
const vp = Math.max(320, window.innerHeight - ctaH - 20);
const target = window.scrollY + rect.top - Math.max(14, (vp - rect.height) / 2);
window.scrollTo({ top: Math.max(0, target), behavior: 'smooth' });
}
function updateTopBar() {
if (!mTopBar) return;
var plan = PLANS[selPlan], extra = selExtra * plan.extra, total = plan.base + extra;
var nameEl = root.querySelector('#mtb-plan');
var priceEl = root.querySelector('#mtb-price');
if (nameEl) nameEl.textContent = C.planNames[selPlan];
if (priceEl) priceEl.textContent = fmt(total) + ' kr';
if (step2Active || step3Active) mTopBar.classList.add('mtb-visible');
else mTopBar.classList.remove('mtb-visible');
}
function revealDevices() {
root.querySelector('#plan-grid').classList.add('collapsed');
root.querySelector('#plan-summary').classList.add('visible');
root.querySelector('#xdev-block').classList.add('xdev-revealed');
root.querySelector('#mstep-1').className = 'm-step done';
root.querySelector('#mstep-2').className = 'm-step active';
root.querySelector('#mstep-3').className = 'm-step';
if (mCtaEl) mCtaEl.classList.add('m-cta-up');
syncCheckoutCtaState();
step2Active = true; step3Active = false;
updateTopBar();
refreshMobileCta();
keepCheckoutInView();
}
function changePlan() {
root.querySelector('#plan-grid').classList.remove('collapsed');
root.querySelector('#plan-summary').classList.remove('visible');
root.querySelector('#xdev-block').classList.remove('xdev-revealed');
var _form = root.querySelector('#lead-form');
var _pre = root.querySelector('#lf-pre-form');
var _div = root.querySelector('#lf-form-divider');
if (_form) _form.classList.remove('lf-shown');
if (_pre) _pre.classList.remove('co-hidden');
if (_div) _div.style.display = 'none';
// Restore form elements to right panel in case they were moved on mobile
var rightPanel = root.querySelector('.summary-panel > .co-panel');
if (rightPanel) {
if (_pre) rightPanel.appendChild(_pre);
if (_div) rightPanel.appendChild(_div);
if (_form) rightPanel.appendChild(_form);
}
if (mCtaEl) mCtaEl.classList.remove('m-cta-up');
syncCheckoutCtaState();
step2Active = false; step3Active = false; planTapped = false;
root.querySelector('#mstep-1').className = 'm-step active';
root.querySelector('#mstep-2').className = 'm-step';
root.querySelector('#mstep-3').className = 'm-step';
updateTopBar();
refreshMobileCta();
keepCheckoutInView();
}
function selectPlan(tile) {
root.querySelectorAll('.plan-tile').forEach(t => t.classList.remove('selected'));
tile.classList.add('selected');
selPlan = tile.dataset.plan;
updateChipPrices();
updateSummary();
if (isMob()) {
if (!planTapped) { planTapped = true; revealDevices(); }
else if (mCtaEl) { mCtaEl.classList.add('m-cta-up'); syncCheckoutCtaState(); }
}
}
function selectExtra(chip) {
root.querySelectorAll('.chip').forEach(c => c.classList.remove('selected'));
chip.classList.add('selected');
selExtra = parseInt(chip.dataset.extra, 10);
updateSummary();
}
// ── Form logic ──────────────────────────────────────────────────────────
function showForm() {
var pre = root.querySelector('#lf-pre-form');
var form = root.querySelector('#lead-form');
var divider = root.querySelector('#lf-form-divider');
if (!form || form.classList.contains('lf-shown')) return;
if (pre) pre.classList.add('co-hidden');
if (divider) divider.style.display = '';
form.classList.add('lf-shown');
if (isMob()) {
// Move form into left panel so step 3 is in the same viewport as steps 1 and 2
var leftPanel = root.querySelector('.checkout-grid > .co-panel');
if (leftPanel) {
if (pre) leftPanel.appendChild(pre);
if (divider) leftPanel.appendChild(divider);
leftPanel.appendChild(form);
}
// Collapse chips — form replaces them
var xdev = root.querySelector('#xdev-block');
if (xdev) xdev.classList.remove('xdev-revealed');
root.querySelector('#mstep-2').className = 'm-step done';
root.querySelector('#mstep-3').className = 'm-step active';
step2Active = false; step3Active = true;
if (mCtaEl) { mCtaEl.classList.add('m-cta-up'); syncCheckoutCtaState(); }
updateTopBar();
refreshMobileCta();
window.scrollTo({ top: 0, behavior: 'smooth' });
} else {
if (mCtaEl) mCtaEl.classList.remove('m-cta-up');
syncCheckoutCtaState();
}
}
function revealEmailField(focusAfter) {
if (emailRevealed) {
if (focusAfter) { var e = root.querySelector('#lf-email'); if (e) e.focus(); }
return;
}
var step = root.querySelector('#lf-step-email');
var submitBtn = root.querySelector('#lf-submit');
if (!step) return;
emailRevealed = true;
step.classList.remove('hidden');
void step.offsetWidth;
step.classList.add('visible');
if (submitBtn) submitBtn.classList.add('lf-submit-shown');
if (focusAfter) { setTimeout(function() { var e = root.querySelector('#lf-email'); if (e) e.focus(); }, 360); }
}
function shakeEl(el) {
if (!el) return;
el.classList.remove('co-lf-shaking');
void el.offsetWidth;
el.classList.add('co-lf-shaking');
el.addEventListener('animationend', function() { el.classList.remove('co-lf-shaking'); }, { once: true });
}
function autoCapture(phone, email) {
try { localStorage.setItem('tvm_lead', JSON.stringify({ phone: phone || '', email: email || '', locale: C.locale, ts: new Date().toISOString() })); } catch(e) {}
}
const AC_DOMAINS = ['gmail.com','hotmail.com','outlook.com','yahoo.com','icloud.com','me.com','live.com','protonmail.com','msn.com','telia.com','bredband.net'];
function attachEmailAC(input, onAccept) {
if (!input) return;
function suggest() {
var val = input.value;
if (input.selectionStart !== val.length) return;
var atIdx = val.indexOf('@');
if (atIdx < 0 && val.length > 0) { var s = val + '@gmail.com'; input.value = s; input.setSelectionRange(val.length, s.length); return; }
if (atIdx < 1) return;
var partial = val.slice(atIdx + 1).toLowerCase();
if (!partial) { var s2 = val + 'gmail.com'; input.value = s2; input.setSelectionRange(val.length, s2.length); return; }
var match = AC_DOMAINS.find(function(d) { return d.startsWith(partial) && d !== partial; });
if (!match) return;
var s3 = val.slice(0, atIdx + 1) + match; input.value = s3; input.setSelectionRange(val.length, s3.length);
}
input.addEventListener('input', suggest);
input.addEventListener('keydown', function(e) {
var sel = input.selectionStart, end = input.selectionEnd, hasSug = sel < end;
if (hasSug) {
if (e.key === 'Tab' || e.key === 'ArrowRight' || e.key === 'Enter') { e.preventDefault(); input.setSelectionRange(end, end); if (e.key === 'Enter' && onAccept) onAccept(); return; }
if (e.key === 'Escape') { e.preventDefault(); input.value = input.value.slice(0, sel); input.setSelectionRange(sel, sel); return; }
} else { if (e.key === 'Enter' && onAccept) { e.preventDefault(); onAccept(); } }
});
}
function escapeHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); }
function initPhoneField() {
var phone = root.querySelector('#lf-phone');
var phoneField = root.querySelector('#lf-phone-field');
if (!phone || !phoneField) return;
function checkPhoneValidity() {
var val = phone.value.trim(), digits = val.replace(/\D/g, '').length, valid = digits >= 8;
phoneField.classList.toggle('co-valid', valid);
phoneField.classList.toggle('co-invalid', !valid && val.length > 2);
return valid;
}
phone.addEventListener('input', function() {
var pos = phone.selectionStart, raw = phone.value.replace(/[^\d\s+\-()]/g, '');
if (raw !== phone.value) { phone.value = raw; try { phone.setSelectionRange(pos, pos); } catch(x){} }
var valid = checkPhoneValidity();
var err = root.querySelector('#lf-phone-err');
if (err && valid) err.classList.remove('show');
if (valid) revealEmailField(false);
});
phone.addEventListener('blur', function() {
if (checkPhoneValidity()) { autoCapture(selectedCC + ' ' + phone.value.trim(), ''); revealEmailField(false); }
});
phone.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
if (checkPhoneValidity()) { revealEmailField(true); }
else { shakeEl(phoneField); var err = root.querySelector('#lf-phone-err'); if (err) err.classList.add('show'); }
}
});
}
function initEmailField() {
var email = root.querySelector('#lf-email');
if (!email) return;
email.addEventListener('blur', function() {
if (/[^@\s]+@[^@\s]+\.[^@\s]+/.test(email.value.trim())) {
autoCapture(selectedCC + ' ' + (root.querySelector('#lf-phone') || {value:''}).value.trim(), email.value.trim());
}
});
email.addEventListener('input', function() {
var err = root.querySelector('#lf-email-err');
if (err && /[^@\s]+@[^@\s]+\.[^@\s]+/.test(email.value.trim())) err.classList.remove('show');
});
attachEmailAC(email, function() { var btn = root.querySelector('#lf-submit'); if (btn) btn.focus(); });
}
function initCCPicker() {
var btn = root.querySelector('#lf-cc-btn');
var dd = root.querySelector('#lf-cc-dd');
var phoneField = root.querySelector('#lf-phone-field');
var searchInput = root.querySelector('#lf-cc-search');
var list = root.querySelector('#lf-cc-list');
if (!btn || !dd || !phoneField) return;
function openDD() { dd.classList.add('open'); phoneField.classList.add('co-lf-cc-open'); btn.setAttribute('aria-expanded','true'); if (searchInput) { searchInput.value=''; filterItems(''); setTimeout(function(){searchInput.focus();},60); } }
function closeDD() { dd.classList.remove('open'); phoneField.classList.remove('co-lf-cc-open'); btn.setAttribute('aria-expanded','false'); }
function filterItems(q) {
var items = (list||dd).querySelectorAll('.co-lf-cc-item'), lq = q.toLowerCase().trim();
items.forEach(function(item) {
var name=(item.dataset.name||item.querySelector('.co-lf-cc-item-name').textContent).toLowerCase();
item.classList.toggle('co-lf-cc-hidden', !(!lq||name.includes(lq)||(item.dataset.code||'').includes(lq)));
});
}
btn.addEventListener('click', function(e) { e.stopPropagation(); dd.classList.contains('open')?closeDD():openDD(); });
if (searchInput) { searchInput.addEventListener('input', function(e){filterItems(e.target.value);}); searchInput.addEventListener('click', function(e){e.stopPropagation();}); searchInput.addEventListener('keydown', function(e){if(e.key==='Escape'){closeDD();root.querySelector('#lf-phone').focus();}}); }
dd.addEventListener('click', function(e) {
var item = e.target.closest('.co-lf-cc-item'); if (!item) return;
selectedCC = item.dataset.code;
root.querySelector('#lf-cc-flag').textContent = item.dataset.flag;
root.querySelector('#lf-cc-code').textContent = selectedCC;
dd.querySelectorAll('.co-lf-cc-item').forEach(function(i){i.classList.toggle('selected',i.dataset.code===selectedCC);});
closeDD(); root.querySelector('#lf-phone').focus();
});
var _ddClose = function(e) { if (!phoneField.contains(e.target)) closeDD(); };
var _ddEsc = function(e) { if (e.key==='Escape') closeDD(); };
document.addEventListener('click', _ddClose);
document.addEventListener('keydown', _ddEsc);
return function() { document.removeEventListener('click', _ddClose); document.removeEventListener('keydown', _ddEsc); };
}
function buildPayload() {
var plan = PLANS[selPlan], total = plan.base + selExtra * plan.extra;
var emailEl = root.querySelector('#lf-email');
return { plan: selPlan, planLabel: C.planNames[selPlan], screens: 1+selExtra, totalPrice: total, currency: C.currency, phone: selectedCC+' '+(root.querySelector('#lf-phone')||{value:''}).value.trim(), email: emailEl ? emailEl.value.trim() : '', locale: C.locale, timestamp: new Date().toISOString(), pageUrl: location.href, captureType: 'full' };
}
async function postToWebhook(payload) {
console.log('[tvm lead]', JSON.stringify(payload, null, 2));
if (!CHECKOUT_WEBHOOK_URL) return { skipped: true };
try {
await fetch(CHECKOUT_WEBHOOK_URL, { method:'POST', mode:'no-cors', headers:{'Content-Type':'text/plain;charset=utf-8'}, body: JSON.stringify(payload) });
return { ok: true };
} catch(err) { console.error('[tvm webhook error]', err); return { error: err.message }; }
}
async function submitLead(e) {
e.preventDefault();
var form = e.target;
if (form.website && form.website.value) return;
var phoneVal = (root.querySelector('#lf-phone')||{value:''}).value.trim();
if (phoneVal.replace(/\D/g,'').length < 8) {
shakeEl(root.querySelector('#lf-phone-field'));
var pErr = root.querySelector('#lf-phone-err'); if (pErr) pErr.classList.add('show');
root.querySelector('#lf-phone').focus(); return;
}
var emailEl = root.querySelector('#lf-email'), emailVal = emailEl ? emailEl.value.trim() : '';
if (!/[^@\s]+@[^@\s]+\.[^@\s]+/.test(emailVal)) {
if (!emailRevealed) { revealEmailField(true); return; }
shakeEl(root.querySelector('.co-lf-email-wrap'));
var eErr = root.querySelector('#lf-email-err'); if (eErr) eErr.classList.add('show');
if (emailEl) emailEl.focus(); return;
}
if (document.activeElement) document.activeElement.blur();
var btn = root.querySelector('#lf-submit');
btn.disabled = true;
btn.innerHTML = '';
var payload = buildPayload();
autoCapture(payload.phone, payload.email);
await postToWebhook(payload);
showSuccess(payload);
}
function showSuccess(p) {
var formEl = root.querySelector('#lead-form');
var panel = (formEl && formEl.closest('.co-panel')) || root.querySelector('.summary-panel .co-panel');
if (!panel) return;
var confColors = ['#ff3b30','#34c759','#0071e3','#ff9500','#af52de','#5ac8fa','#ff2d55','#ffd60a'];
var confHtml = '';
for (var ci = 0; ci < 14; ci++) {
var angle=(ci/14)*2*Math.PI, dist=38+Math.round(Math.random()*28);
var tx=Math.round(Math.sin(angle)*dist), ty=Math.round(-Math.abs(Math.cos(angle))*dist-10);
var r=Math.round(Math.random()*360), d=(ci*0.045).toFixed(2);
confHtml += '';
}
var callLine = C.successCallLine.replace('{phone}',''+escapeHtml(p.phone)+'');
var footLine = C.successFootnote.replace('{email}',''+escapeHtml(p.email)+'');
panel.innerHTML =
''+
'
'+confHtml+
'
'+
'
'+
'
'+
'
'+C.successHeading+'
'+
'
'+C.successSavingsHero.replace('7 884 kr','7 884 kr')+'
'+
'
'+
'
'+C.successBeforeLbl+'
'+C.successFromBefore+'
'+
'
→
'+
'
'+C.successAfterLbl+'
'+C.successFromAfter+'
'+
'
'+
'
'+C.successFromNote+'
'+
'
'+
'
'+
'
'+callLine+'
'+C.successHumanNote+'
'+
'
'+
'
'+C.successWelcome.replace('2 400+','2 400+')+'
'+
'
'+footLine+'
'+
'
';
if (mTopBar) mTopBar.classList.remove('mtb-visible');
if (mCtaEl) mCtaEl.classList.remove('m-cta-up');
step2Active = false; step3Active = false;
syncCheckoutCtaState();
try { localStorage.removeItem('tvm_lead'); } catch(x) {}
}
// Event wiring
root.querySelectorAll('.plan-tile').forEach(t => t.addEventListener('click', () => selectPlan(t)));
root.querySelectorAll('.chip').forEach(c => c.addEventListener('click', () => selectExtra(c)));
root.querySelector('.plan-summary')?.addEventListener('click', changePlan);
root.querySelector('#mtb-edit-btn')?.addEventListener('click', changePlan);
var buyBtn = root.querySelector('#lf-buy-btn');
if (buyBtn) buyBtn.addEventListener('click', showForm);
var leadForm = root.querySelector('#lead-form');
if (leadForm) leadForm.addEventListener('submit', submitLead);
updateChipPrices();
updateSummary();
initPhoneField();
var cleanupCC = initCCPicker();
initEmailField();
(function initViewers() {
var el = root.querySelector('#co-viewers-count'); if (!el) return;
var n = 14 + Math.floor(Math.random() * 18); el.textContent = n;
setInterval(function() { n = Math.max(8, Math.min(40, n + Math.floor(Math.random() * 5) - 2)); el.textContent = n; }, 3500 + Math.random() * 3000);
})();
(function initSlots() {
var el = root.querySelector('#co-slots-count'); if (!el) return;
var n = 5 + Math.floor(Math.random() * 4); el.textContent = n;
function tick() { if (n <= 2) return; n--; el.textContent = n; setTimeout(tick, 60000 + Math.random() * 60000); }
setTimeout(tick, 45000 + Math.random() * 30000);
})();
let snapCooled = false, snapTimer = null, lastClickInSection = 0;
function noteCheckoutClick() { lastClickInSection = Date.now(); snapCooled = true; setTimeout(() => { snapCooled = false; }, 1800); }
root.addEventListener('pointerdown', noteCheckoutClick, { passive: true });
root.addEventListener('click', noteCheckoutClick);
function snapToSection() {
if (snapCooled || !isMob()) return;
if (Date.now() - lastClickInSection < 2000) return;
const rect = root.getBoundingClientRect();
const partial = rect.top < window.innerHeight * 0.7 && rect.top > -rect.height * 0.3;
if (partial && Math.abs(rect.top) > 4) { snapCooled = true; keepCheckoutInView(); setTimeout(() => { snapCooled = false; }, 1200); }
}
function refreshMobileCta() {
if (!mCtaEl || !mCtaBtn) return;
if (!isMob()) { mCtaEl.classList.remove('m-cta-up'); syncCheckoutCtaState(); return; }
const rect = (checkoutPanel || root).getBoundingClientRect();
const inView = rect.top < window.innerHeight - 80 && rect.bottom > 80;
if (!inView) { mCtaEl.classList.remove('m-cta-up'); syncCheckoutCtaState(); return; }
if (step3Active) {
mCtaEl.classList.add('m-cta-up');
mCtaBtn.textContent = C.mCtaStep3;
mCtaBtn.onclick = function() { var f = root.querySelector('#lead-form'); if (f) f.dispatchEvent(new Event('submit', {bubbles:true,cancelable:true})); };
} else if (step2Active) {
mCtaEl.classList.add('m-cta-up');
mCtaBtn.textContent = C.mCtaStep2;
mCtaBtn.onclick = showForm;
} else {
mCtaBtn.textContent = C.mCtaStep1;
mCtaBtn.onclick = revealDevices;
}
syncCheckoutCtaState();
}
function onScroll() { refreshMobileCta(); clearTimeout(snapTimer); snapTimer = setTimeout(snapToSection, 120); }
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', refreshMobileCta);
refreshMobileCta();
return () => {
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', refreshMobileCta);
root.removeEventListener('pointerdown', noteCheckoutClick);
root.removeEventListener('click', noteCheckoutClick);
clearTimeout(snapTimer);
document.body.classList.remove('checkout-cta-active');
document.documentElement.classList.remove('checkout-cta-active');
if (typeof cleanupCC === 'function') cleanupCC();
};
}, []);
const PC = PRICING_COPY[window.locale || 'SE'];
const PP = PRICING_PLANS[window.locale || 'SE'];
return (
1 År
—
699 kr
Priser
Välj ditt paket.
Börja streama direkt.
{PC.panelLabel}
{PC.sectionLabel}
{PP['3m'].base.toLocaleString(PC.numLocale)}kr{PC.planSub['3m']}
~116 kr/mån –84%
{PC.planNames['1y']}
{PC.popularBadge}
{PP['1y'].base.toLocaleString(PC.numLocale)}kr{PC.planSub['1y']}
~58 kr/mån –92%
{PP['3y'].base.toLocaleString(PC.numLocale)}kr{PC.planSub['3y']}
~31 kr/mån –96%
{PC.devicesTitle}
1
{PC.chipUnit1}
{PC.chipIncluded}
{PC.chipPop}
2
{PC.chipUnitN}
{PC.devicesNote}
{PC.summaryLabel}
{PC.sumPlan}1 År
{PC.sumBase}699 kr
{PC.sumTotal}
699kr
{PC.orderNote}
);
}
function FAQ() {
const listRef = React.useRef(null);
const C = COPY[window.locale];
React.useEffect(() => {
const list = listRef.current;
if (!list) return;
const cards = Array.from(list.querySelectorAll('.faq-item'));
const faqSection = list.closest('.faq');
let openCount = 0;
let ctaUpgraded = false;
function upgradeWhatsappCta() {
if (ctaUpgraded || !faqSection) return;
const el = faqSection.querySelector('.faq-wa-link');
if (!el) return;
ctaUpgraded = true;
el.classList.add('faq-wa-primary');
el.setAttribute('aria-label', COPY[window.locale].waAriaLabel);
if (!el.querySelector('svg')) {
const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
icon.setAttribute('width', '18');
icon.setAttribute('height', '18');
icon.setAttribute('viewBox', '0 0 24 24');
icon.setAttribute('fill', 'white');
icon.setAttribute('aria-hidden', 'true');
icon.setAttribute('style', 'flex-shrink:0');
icon.innerHTML = '';
el.insertBefore(icon, el.firstChild);
}
}
cards.forEach(card => {
const trigger = card.querySelector('.faq-q');
function toggle(e) {
e.stopPropagation();
const isOpen = card.classList.contains('open');
cards.forEach(c => {
c.classList.remove('open');
c.querySelector('.faq-q').setAttribute('aria-expanded', 'false');
});
if (!isOpen) {
card.classList.add('open');
trigger.setAttribute('aria-expanded', 'true');
openCount += 1;
if (openCount >= 3) upgradeWhatsappCta();
}
}
trigger.addEventListener('click', toggle);
trigger.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(e); }
});
});
}, []);
return (
{C.faqTitle}
{C.faqItems.map(([q, a], i) => (
))}
);
}
function FinalCta() {
const C = COPY[window.locale];
return (
);
}
function Footer() {
const C = COPY[window.locale];
return (
);
}
function RecentlyBought() {
const _C = COPY[window.locale];
const [p0, p1, p2] = _C.rbPlans;
const purchases = [
{ name:"Erik J.", plan: p0, where:"Stockholm", ago: _C.rbNow, color:"#6e6e73" },
{ name:"Amina K.", plan: p1, where:"Göteborg", ago:"1 min", color:"#8e8e93" },
{ name:"Lars P.", plan: p2, where:"Oslo", ago:"3 min", color:"#636366" },
{ name:"Sara H.", plan: p0, where:"Malmö", ago:"5 min", color:"#8e8e93" },
{ name:"Mohammed A.", plan: p1, where:"Linköping", ago:"7 min", color:"#6e6e73" },
{ name:"Ingrid N.", plan: p0, where:"Bergen", ago:"9 min", color:"#8e8e93" },
];
const CYCLE = 4500;
const STEP_MS = 80;
const [idx, setIdx] = React.useState(0);
const [out, setOut] = React.useState(false);
const [barPct, setBarPct] = React.useState(0);
const barTimer = React.useRef(null);
const startBar = React.useCallback(() => {
clearInterval(barTimer.current);
setBarPct(0);
let pct = 0;
barTimer.current = setInterval(() => {
pct = Math.min(100, pct + (STEP_MS / CYCLE) * 100);
setBarPct(pct);
if (pct >= 100) clearInterval(barTimer.current);
}, STEP_MS);
}, []);
React.useEffect(() => {
startBar();
const cycle = setInterval(() => {
setOut(true);
setTimeout(() => {
setIdx(i => (i + 1) % purchases.length);
setOut(false);
startBar();
}, 400);
}, CYCLE);
return () => { clearInterval(cycle); clearInterval(barTimer.current); };
}, [startBar]);
const p = purchases[idx];
return (
{p.name[0]}
{_C.rbEyebrow}
{p.name}
{p.plan} · {p.where}
);
}
function Testimonials() {
React.useEffect(() => {
const DIR = 'assets/ugc/';
const SPEED = 0.45;
const GAP = 14;
const VID_RE = /\.(webm|mp4|mov)$/i;
const IMG_RE = /\.(webp|jpg|jpeg|png)$/i;
const REVIEWS = COPY[window.locale].ugcReviews;
const track1 = document.getElementById('ugc-track-1');
const track2 = document.getElementById('ugc-track-2');
const wrap1 = document.getElementById('ugc-wrap-1');
const wrap2 = document.getElementById('ugc-wrap-2');
if (!track1 || !track2) return;
let offset1 = 0, setWidth1 = 0;
let offset2 = 0, setWidth2 = 0;
let rafId = null, isPaused = false;
function tick() {
if (!isPaused) {
offset1 -= SPEED;
if (offset1 <= -setWidth1) offset1 += setWidth1;
track1.style.transform = `translateX(${offset1}px)`;
offset2 += SPEED;
if (offset2 >= 0) offset2 -= setWidth2;
track2.style.transform = `translateX(${offset2}px)`;
}
rafId = requestAnimationFrame(tick);
}
function wireVideo(vid, bg, btn, cardEl) {
vid.muted = true; vid.loop = true;
['muted','playsinline','webkit-playsinline','x5-playsinline'].forEach(a => vid.setAttribute(a, ''));
vid.setAttribute('preload', 'none');
const show = () => { if (bg) bg.style.opacity='0'; if (btn) btn.classList.add('ugc-play-hidden'); };
const hide = () => { if (btn) btn.classList.remove('ugc-play-hidden'); };
vid.addEventListener('playing', show); vid.addEventListener('pause', hide);
vid.addEventListener('waiting', hide); vid.addEventListener('stalled', hide);
vid.addEventListener('error', () => { vid.style.display='none'; if(bg) bg.style.opacity='1'; if(btn) btn.style.display='none'; });
const io = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) {
if (!vid.querySelector('source')) {
if (vid.dataset.mp4) { const s=document.createElement('source'); s.src=vid.dataset.mp4; s.type='video/mp4'; vid.appendChild(s); }
if (vid.dataset.webm) { const s=document.createElement('source'); s.src=vid.dataset.webm; s.type='video/webm; codecs="vp9,opus"'; vid.appendChild(s); }
vid.load();
}
vid.play().catch(() => {});
} else { vid.pause(); }
});
}, { threshold: 0.1, rootMargin: '0px 80px' });
io.observe(cardEl);
cardEl.addEventListener('click', () => { vid.paused ? vid.play().catch(()=>{}) : vid.pause(); });
}
function makeVideo(src) {
const card = document.createElement('div'); card.className = 'ugc-card ugc-video';
const bg = document.createElement('div'); bg.className = 'ugc-vid-bg'; card.appendChild(bg);
const vid = document.createElement('video');
if (/\.(mp4|mov)$/i.test(src)) { vid.dataset.mp4 = DIR + src; }
else if (/\.webm$/i.test(src)) { vid.dataset.mp4 = DIR + src.replace(/\.webm$/i, '.mp4'); vid.dataset.webm = DIR + src; }
card.appendChild(vid);
const btn = document.createElement('div'); btn.className = 'ugc-play';
btn.innerHTML = '';
card.appendChild(btn);
wireVideo(vid, bg, btn, card);
return card;
}
let imgIdx = 0;
function makeImage(src) {
const card = document.createElement('div'); card.className = 'ugc-card ugc-img';
const img = document.createElement('img'); img.src = DIR + src; img.alt = 'Kundrecension Tvmomento ' + (++imgIdx); img.loading = 'lazy'; img.decoding = 'async';
card.appendChild(img); return card;
}
const STAR = '';
function makeTpCard(r) {
const card = document.createElement('div'); card.className = 'ugc-card ugc-tp';
const head = document.createElement('div'); head.className = 'ugc-tp-head';
const stars = document.createElement('div'); stars.className = 'ugc-tp-stars'; stars.innerHTML = STAR.repeat(5);
const logo = document.createElement('span'); logo.className = 'ugc-tp-logo'; logo.textContent = 'Trustpilot';
head.appendChild(stars); head.appendChild(logo); card.appendChild(head);
const txt = document.createElement('p'); txt.className = 'ugc-tp-text';
txt.textContent = '“' + r.text + '”'; card.appendChild(txt);
const foot = document.createElement('div'); foot.className = 'ugc-tp-foot';
const name = document.createElement('span'); name.className = 'ugc-tp-name'; name.textContent = r.name;
const badge = document.createElement('span'); badge.className = 'ugc-tp-badge'; badge.textContent = COPY[window.locale].ugcVerified;
foot.appendChild(name); foot.appendChild(badge); card.appendChild(foot);
return card;
}
function cloneCard(orig) {
const clone = orig.cloneNode(true);
const ov = orig.querySelector('video'), cv = clone.querySelector('video');
if (ov && cv) {
cv.dataset.mp4 = ov.dataset.mp4 || ''; cv.dataset.webm = ov.dataset.webm || '';
Array.from(cv.querySelectorAll('source')).forEach(s => s.remove());
cv.setAttribute('preload', 'none');
wireVideo(cv, clone.querySelector('.ugc-vid-bg'), clone.querySelector('.ugc-play'), clone);
}
return clone;
}
async function init() {
let files = [
'customer-review-1.webp',
'customer-review-2.webp',
'customer-review-3.webp',
'customer-review-4.webp',
'customer-review-5.webp'
];
if (!files.length) return;
const vids = files.filter(f => VID_RE.test(f)), imgs = files.filter(f => IMG_RE.test(f));
const mediaList = []; let vi = 0, ii = 0;
while (vi < vids.length || ii < imgs.length) {
if (vi < vids.length) mediaList.push({ v: true, s: vids[vi++] });
if (ii < imgs.length) mediaList.push({ v: false, s: imgs[ii++] });
}
// pattern: [media, tp, tp] per group; phaseOffset shifts which slot starts
function buildCards(phaseOffset) {
const cards = []; const groupCount = Math.max(mediaList.length, 4);
let mi = 0, ri = 0;
for (let g = 0; g < groupCount; g++) {
for (let p = 0; p < 3; p++) {
const slot = (p + phaseOffset) % 3;
if (slot === 0) { const m = mediaList[mi % mediaList.length]; mi++; cards.push(m.v ? makeVideo(m.s) : makeImage(m.s)); }
else { cards.push(makeTpCard(REVIEWS[ri % REVIEWS.length])); ri++; }
}
}
return cards;
}
// row 1: media → tp → tp → … row 2: tp → tp → media → …
const cards1 = buildCards(0); cards1.forEach(c => track1.appendChild(c));
const cards2 = buildCards(1); cards2.forEach(c => track2.appendChild(c));
requestAnimationFrame(() => requestAnimationFrame(() => {
setWidth1 = cards1.reduce((sum, c) => sum + c.offsetWidth + GAP, 0);
setWidth2 = cards2.reduce((sum, c) => sum + c.offsetWidth + GAP, 0);
const minW = (window.innerWidth || 800) * 3;
let n1 = 0; while (n1 * setWidth1 < minW) { cards1.forEach(c => track1.appendChild(cloneCard(c))); n1++; }
let n2 = 0; while (n2 * setWidth2 < minW) { cards2.forEach(c => track2.appendChild(cloneCard(c))); n2++; }
offset2 = -setWidth2; // row 2 starts right, scrolls left-to-right
tick();
}));
}
document.addEventListener('visibilitychange', () => { isPaused = document.hidden; });
init();
return () => { if (rafId) cancelAnimationFrame(rafId); };
}, []);
const C_ugc = COPY[window.locale];
return (
{C_ugc.ugcEyebrow}
{C_ugc.ugcH1}
{C_ugc.ugcH2}
);
}
function WhatsAppFloat() {
return (
{ e.currentTarget.style.transform='scale(1.1)'; e.currentTarget.style.boxShadow='0 8px 32px rgba(37,211,102,0.5), 0 4px 12px rgba(0,0,0,0.18)'; }}
onMouseLeave={e => { e.currentTarget.style.transform='scale(1)'; e.currentTarget.style.boxShadow='0 4px 20px rgba(37,211,102,0.4), 0 2px 8px rgba(0,0,0,0.15)'; }}
>
);
}
Object.assign(window, {
ReceiptBad, ReceiptGood, ReceiptCompare,
FeatureSavings, HowItWorks, DeviceCompat, Testimonials,
Pricing, FAQ, FinalCta, Footer, RecentlyBought, WhatsAppFloat,
});