/* Powermieter — App shell: state, chrome, device frame, routing */ const { useState: useS, useEffect: useE, useRef: useR } = React; const LS = { get: (k, d) => { try { const v = localStorage.getItem('pm_' + k); return v == null ? d : JSON.parse(v); } catch { return d; } }, set: (k, v) => { try { localStorage.setItem('pm_' + k, JSON.stringify(v)); } catch {} }, }; const INITIAL_FORM = { vorname: '', nachname: '', email: '', telefon: '', adresse: '', consent1: false, geburt: '', strasse: '', hausnr: '', plz: '', ort: '', aufgang: '', etage: '', whgnr: '', klingel: '', anbieter: '', kundennr: '', zaehler: '', zaehlerUnbekannt: false, verbrauch: '', lieferbeginnTyp: 'fruehest', lieferbeginn: '', dsgvo: false, digital: true, v_agb: false, v_widerruf: false, v_digital: false, }; const SAMPLE_FORM = { vorname: 'Max', nachname: 'Mustermann', email: 'max@beispiel.de', telefon: '+49 170 1234567', adresse: 'Musterstraße 12, 2. OG rechts', consent1: true, strasse: 'Musterstraße', hausnr: '12', plz: '10115', ort: 'Berlin', aufgang: 'Haus A', etage: '2. OG', whgnr: 'W04', anbieter: 'Stadtwerke Berlin', zaehler: '1 ESY 0123 4567', verbrauch: '2.500', lieferbeginnTyp: 'fruehest', dsgvo: true, digital: true, v_agb: true, v_widerruf: true, v_digital: true, }; /* Tweaks: simulate the contract gatekeeper without touching the data */ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "goLiveGate": "offen", "datenfuelle": "lueckenhaft", "quote": 58 }/*EDITMODE-END*/; function TweaksUI({ t, setTweak }) { if (!window.TweaksPanel) return null; return ( setTweak('goLiveGate', v)} /> setTweak('datenfuelle', v)} /> setTweak('quote', v)} /> ); } function renderScreen(step, props) { if (step === 1) return ; if (step === 2) return ; if (step === 3 && window.Screen3) return ; if (step === 4 && window.Screen4) return ; return ; } /* Landing (Einstieg) rendered natively inside the prototype — reuses the landing-page sections; CTAs hand off into the onboarding flow. */ function LandingEntry({ onEnter }) { const [aud, setAud] = useS('mieter'); const [chat, setChat] = useS(false); const toForm = () => window.lpScrollTo && window.lpScrollTo('lp-form'); if (!window.HeroSection) return ; return (
setChat(true)} /> setChat(true)} /> onEnter(2)} /> setChat(true)} />
{window.PaulaChat && setChat(true)} onClose={() => setChat(false)} />} {window.StickyCTA && }
); } function App() { const [theme, setTheme] = useS(() => LS.get('theme', 'light')); const [device, setDevice] = useS(() => LS.get('device', 'mobile')); const [view, setView] = useS(() => LS.get('view', 'landing')); // 'landing' | 'flow' | 'admin' const [step, setStep] = useS(() => LS.get('step', 1)); const [maxReached, setMax] = useS(() => LS.get('max', 1)); const [form, setForm] = useS(() => ({ ...INITIAL_FORM, ...LS.get('form', {}) })); const [errors, setErrors] = useS({}); const [model, setModel] = useS(() => LS.get('model_v', 'mieterstrom')); const [t, setTweak] = (window.useTweaks || (() => [TWEAK_DEFAULTS, () => {}]))(TWEAK_DEFAULTS); useE(() => LS.set('model_v', model), [model]); useE(() => { document.documentElement.setAttribute('data-theme', theme); LS.set('theme', theme); }, [theme]); useE(() => LS.set('device', device), [device]); useE(() => LS.set('view', view), [view]); useE(() => LS.set('step', step), [step]); useE(() => LS.set('max', maxReached), [maxReached]); useE(() => LS.set('form', form), [form]); // Landing (embedded iframe) → Flow handoff useE(() => { const onMsg = (e) => { const d = e.data; if (d && d.pm === 'enterFlow') { const n = Math.min(Math.max(parseInt(d.step) || 1, 1), 4); setView('flow'); setStep(n); setMax(m => Math.max(m, n)); } }; window.addEventListener('message', onMsg); return () => window.removeEventListener('message', onMsg); }, []); const scrollRef = useR(null); const set = (k, v) => setForm(f => ({ ...f, [k]: v })); // safe scroll helper (no scrollIntoView) useE(() => { const scroller = (id) => { const el = document.getElementById(id); const sc = scrollRef.current; if (!el || !sc) return; const top = el.getBoundingClientRect().top - sc.getBoundingClientRect().top + sc.scrollTop - 16; sc.scrollTo({ top, behavior: 'smooth' }); }; window.pmScrollTo = scroller; window.lpScrollTo = scroller; }, []); const scrollTop = () => { if (scrollRef.current) scrollRef.current.scrollTo({ top: 0, behavior: 'smooth' }); }; const validate = (s) => { const e = {}; const req = (k, msg) => { if (!String(form[k] || '').trim()) e[k] = msg || 'Pflichtfeld'; }; const email = () => { if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(form.email || '')) e.email = 'Bitte gültige E-Mail eingeben'; }; if (s === 1) { req('vorname', 'Bitte Vornamen eingeben'); req('nachname', 'Bitte Nachnamen eingeben'); email(); req('adresse', 'Bitte Adresse / Wohnung angeben'); if (!form.consent1) e.consent1 = 'Bitte stimmen Sie der Kontaktaufnahme zu.'; } if (s === 2) { req('vorname'); req('nachname'); email(); req('strasse', 'Bitte Straße angeben'); req('hausnr', 'Pflichtfeld'); req('plz', 'PLZ angeben'); req('ort', 'Ort angeben'); if (!form.zaehlerUnbekannt) req('zaehler', 'Zählernummer angeben oder "kenne ich nicht" wählen'); if (form.lieferbeginnTyp !== 'fruehest' && !form.lieferbeginn) e.lieferbeginn = 'Bitte Datum wählen'; if (!form.dsgvo) e.dsgvo = 'Bitte dem Datenschutzhinweis zustimmen.'; } if (s === 3) { if (!form.v_agb) e.v_agb = 'Erforderlich'; if (!form.v_widerruf) e.v_widerruf = 'Erforderlich'; if (!form.v_digital) e.v_digital = 'Erforderlich'; } return e; }; const goNext = () => { const e = validate(step); setErrors(e); if (Object.keys(e).length) { // scroll to first error requestAnimationFrame(() => { const el = scrollRef.current?.querySelector('.input-err, .field-err'); if (el && scrollRef.current) { const top = el.getBoundingClientRect().top - scrollRef.current.getBoundingClientRect().top + scrollRef.current.scrollTop - 100; scrollRef.current.scrollTo({ top, behavior: 'smooth' }); } }); return; } const ns = Math.min(step + 1, 4); setStep(ns); setMax(m => Math.max(m, ns)); scrollTop(); }; const goBack = () => { setStep(s => Math.max(1, s - 1)); scrollTop(); }; const jump = (n) => { if (n <= maxReached) { setStep(n); setErrors({}); scrollTop(); } }; const restart = () => { setForm({ ...INITIAL_FORM }); setStep(1); setMax(1); setErrors({}); setView('flow'); scrollTop(); }; const screenProps = { form, set, errors, onNext: goNext, onBack: goBack, device, jump, restart, model, tweaks: t }; const mobile = device === 'mobile'; const Product = (
{/* In-app top bar */}
{!mobile && (
Geprüfter Mieterstrom-Anbieter Hilfe & Kontakt
)} {mobile && }
{/* Stepper */}
{mobile && (
Schritt {step}/4 {STEPS[step - 1].label}
)}
{/* Screen */}
{renderScreen(step, screenProps)}
); const Admin = window.AdminView ? : ; return (
{/* Prototype chrome */}
Prototyp
{view === 'flow' && ( }, { value: 'desktop', label: 'Desktop', icon: }, ]} /> )}
{/* Body */} {view === 'landing' ? (
{ setView('flow'); setStep(n); setMax(m => Math.max(m, n)); scrollTop(); }} />
) : view === 'admin' ? (
{Admin}
) : mobile ? (
{Product}
Mobile Ansicht · QR-Einstieg vom Hausaushang
) : (
{Product}
)}
); } function Placeholder({ n, admin }) { return (

{admin ? 'Hausverwaltung' : 'Schritt ' + n}

Wird gerade gebaut …

); } /* ---------- Embed mode: a single screen, no chrome, no persistence ---------- Used by the overview canvas via ?embed=1&view=flow&step=2&device=mobile&theme=dark */ function EmbedApp() { const p = new URLSearchParams(location.search); const theme = p.get('theme') || 'light'; const device = p.get('device') || 'desktop'; const view = p.get('view') || 'flow'; const step = parseInt(p.get('step') || '1'); const mobile = device === 'mobile'; const [form, setForm] = useS(() => ({ ...INITIAL_FORM, ...SAMPLE_FORM })); const set = (k, v) => setForm(f => ({ ...f, [k]: v })); useE(() => { document.documentElement.setAttribute('data-theme', theme); document.body.classList.add('pm-embed-body'); }, [theme]); const noop = () => {}; const tweaks = { goLiveGate: p.get('gate') || 'offen', datenfuelle: p.get('data') || 'lueckenhaft', quote: parseInt(p.get('quote') || '58') }; const model = p.get('model') || 'mieterstrom'; const props = { form, set, errors: {}, onNext: noop, onBack: noop, device, jump: noop, restart: noop, model, tweaks }; if (view === 'wizard') return
{window.ContractWizard ? u.whg === p.get('unit')) : null} onClose={noop} /> : null}
; if (view === 'admin') return
{window.AdminView ? : null}
; return (
{!mobile ?
Geprüfter Mieterstrom-AnbieterHilfe & Kontakt
: }
{mobile &&
Schritt {step}/4{STEPS[step - 1].label}
}
{renderScreen(step, props)}
); } const IS_EMBED = new URLSearchParams(location.search).get('embed') === '1'; ReactDOM.createRoot(document.getElementById('root')).render(IS_EMBED ? : );