/* ============================================================ Ueber mich — About Thomas Basting Cyan-on-dark, fits Smartmacherei brand ============================================================ */ const UM_PALETTE = { bg: "#071520", card: "#0D2030", accent: "#3ABDD0", soft: "#8DDBEA", ink: "#ffffff", muted: "rgba(141,219,234,0.55)", line: "rgba(141,219,234,0.16)", }; const UM_TIMELINE = [ { year: "Seit 2024", role: "Smartmacherei", org: "Selbständig · Edt bei Lambach", body: "Smart-Home- und Gebäudeautomations-Integration für Altbau, Neubau und kleine Gewerbebetriebe. Fokus auf Home Assistant, Loxone, Energiemanagement und Netzwerk.", accent: true, }, { year: "2021 — 2024", role: "Peneder Bau-Elemente", org: "Produktentwicklung → Vertrieb", body: "Mitentwicklung und Markteinführung einer Türhardware-Produktlinie inkl. Smart-Home-Integration.", }, { year: "2017 — 2021", role: "KUKA CEE GmbH", org: "Anwendungsingenieur → Teamleiter Engineering", body: "Roboter-Projekte von Konzept bis Schulung. Führung des Engineering-Teams, Standardisierung von Pflichtenheften.", }, { year: "2016 — 2017", role: "GTech Automatisierungstechnik", org: "SPS-Programmierer", body: "Steuerungsentwicklung in Siemens TIA / Step 7, Funktionsprüfung und Inbetriebnahme.", }, { year: "2014 — 2016", role: "TGW Mechanics GmbH", org: "Inbetriebnahmeingenieur", body: "Europaweite Inbetriebnahme hochautomatisierter Logistikanlagen.", }, { year: "2007 — 2013", role: "HTL Wels", org: "Mechatronik · Schwerpunkt Automatisierung", body: "Technische Grundausbildung — SPS, Robotik, Elektrotechnik, CAD.", }, ]; const UM_SKILLS = [ { group: "Smart Building", items: ["Home Assistant", "Loxone Planung & Integration", "KNX-Grundlagen", "Matter / Thread / Zigbee"] }, { group: "Automatisierung", items: ["SPS · SIMATIC S7 / TIA", "Industrierobotik · KUKA", "Modbus / MQTT / APIs", "Inbetriebnahme & Schulung"] }, { group: "Energie & Netzwerk", items: ["PV / Wärmepumpe / Wallbox", "Energie-Dashboards", "UniFi / Netzwerk-Planung", "Dokumentation & Backup"] }, { group: "Projekt & Beratung", items: ["Pflichtenheft & Anforderung", "Technische Kundenberatung", "Schnittstellenplanung", "Teamführung"] }, ]; function ContactForm() { const [data, setData] = React.useState({ name: '', email: '', phone: '', path: '', subject: '', message: '', }); const [status, setStatus] = React.useState({ kind: 'idle', msg: '' }); const [floorplanToken, setFloorplanToken] = React.useState(null); const set = (k) => (e) => setData({ ...data, [k]: e.target.value }); // Bei Splash-Auswahl ist der Pfad in localStorage. Vorbelegen, falls vorhanden. // Auch: floorplan-Token vom Konfigurator-Upload anzeigen. // Plus: Konfigurator-Summary aus localStorage in die Nachricht uebernehmen // wenn der User "Termin vereinbaren" geklickt hat. const readKfgSummary = () => { try { const summary = window.localStorage.getItem('sm-kfg-summary'); const kfgPath = window.localStorage.getItem('sm-kfg-path'); if (summary) { setData((d) => { const alreadyHas = d.message && d.message.includes('Konfigurator-Empfehlung'); return { ...d, message: alreadyHas ? d.message : ( `Hallo Thomas,\n\nich habe deinen Konfigurator durchlaufen — hier die Zusammenfassung:\n\n${summary}\n\nKönnen wir dazu einen Termin vereinbaren?` ), path: d.path || kfgPath || '', subject: d.subject || 'Termin-Anfrage aus Konfigurator', }; }); } } catch (e) { /* private mode */ } }; // Paket-CTA aus PaketeSection: subject + Vorlage-Message vorbelegen. // force=true bei explizitem Klick (Event) -- überschreibt auch ein // bereits gesetztes Subject. force=false beim ersten Mount -- nur // wenn der User selbst noch nichts geschrieben hat. const readPaketSubject = (force) => { try { const subj = window.localStorage.getItem('sm-paket-subject'); if (!subj) return; setData((d) => { if (!force && d.subject) return d; const intro = `Hallo Thomas,\n\nich interessiere mich für "${subj.replace(/^Paket-Anfrage:\s*/,'')}".\n\nKurz zur Ausgangssituation:\n\n\nBitte um Rückmeldung — danke!`; return { ...d, subject: subj, message: d.message ? d.message : intro, }; }); } catch (e) { /* private mode */ } }; React.useEffect(() => { try { const saved = window.localStorage.getItem('sm-path'); if (saved && !data.path) setData((d) => ({ ...d, path: saved })); const tok = window.localStorage.getItem('sm-floorplan-token'); if (tok) setFloorplanToken(tok); } catch (e) { /* localStorage in some private modes throws */ } // Beim Mount Summary lesen + auf Custom-Event reagieren wenn Konfigurator // gerade "Termin vereinbaren" geklickt hat (Form ist evtl. schon im DOM). readKfgSummary(); // Paket-CTA prefill (sofort + auf Click-Event aus PaketCard). readPaketSubject(false); const handler = () => readKfgSummary(); const paketHandler = () => readPaketSubject(true); window.addEventListener('sm-kfg-summary-updated', handler); window.addEventListener('sm-paket-cta', paketHandler); return () => { window.removeEventListener('sm-kfg-summary-updated', handler); window.removeEventListener('sm-paket-cta', paketHandler); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // UTM-Attribution aus localStorage (von track.js gesetzt) holen. // Wird mit jeder Anfrage mitgesendet, damit Conversions Kampagnen // zugeordnet werden koennen. const readAttribution = () => { try { const raw = window.localStorage.getItem('sm-attr'); if (!raw) return {}; const a = JSON.parse(raw); const out = {}; ['utm_source','utm_medium','utm_campaign','utm_content','utm_term'].forEach((k) => { if (a[k]) out[k] = a[k]; }); if (a.landing) out.landing_page = a.landing; return out; } catch (e) { return {}; } }; const submit = async (e) => { e.preventDefault(); if (!data.name.trim() || !data.email.trim() || !data.message.trim()) { setStatus({ kind: 'err', msg: 'Bitte Name, E-Mail und Nachricht ausfüllen.' }); return; } setStatus({ kind: 'sending', msg: '' }); try { const payload = { ...data, ...readAttribution(), source: 'website-kontakt', sentAt: new Date().toISOString(), userAgent: navigator.userAgent, }; if (floorplanToken) payload.floorplan_token = floorplanToken; const r = await fetch('/api/anfrage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const json = await r.json().catch(() => ({})); setStatus({ kind: 'ok', msg: json.message + (json.floorplan_attached ? ' Dein Plan wurde mitgeschickt.' : ''), }); setData({ name: '', email: '', phone: '', path: '', subject: '', message: '' }); // Token nach erfolgreichem Versand lokal entfernen -- Server hat den Plan // bereits geloescht (DSGVO). Auch den Konfigurator-Summary kicken. try { window.localStorage.removeItem('sm-floorplan-token'); window.localStorage.removeItem('sm-kfg-summary'); window.localStorage.removeItem('sm-kfg-path'); } catch (e) {} setFloorplanToken(null); // Conversion an Marketing-Pixel + eigenes Reichweiten-Logging // schicken — beide ruhen still wenn Consent fehlt bzw. DNT gesetzt ist. try { if (typeof window.smConvert === 'function') { window.smConvert('lead', { content_category: data.path || 'unknown' }); } if (navigator.sendBeacon) { const ev = JSON.stringify({ event: 'conversion', path: location.pathname, ...readAttribution(), }); navigator.sendBeacon('/api/track', new Blob([ev], { type: 'application/json' })); } } catch (e) { /* fail-quiet */ } } catch (err) { setStatus({ kind: 'err', msg: 'Senden hat gerade nicht geklappt. Schreib mir bitte direkt an thobasting@gmail.com.', }); } }; const sending = status.kind === 'sending'; return (

Anfrage schicken

Antwort in 2 Werktagen