1// site/page-kontakt.jsx 2 3function VideocallModal({ T, onClose }) { 4 const today = new Date(); today.setHours(0,0,0,0); 5 6 const [viewYear, setViewYear] = React.useState(today.getFullYear()); 7 const [viewMonth, setViewMonth] = React.useState(today.getMonth()); 8 const [selDate, setSelDate] = React.useState(null); 9 const [selTime, setSelTime] = React.useState(''); 10 const [name, setName] = React.useState(''); 11 const [email, setEmail] = React.useState(''); 12 const [sent, setSent] = React.useState(false); 13 14 const TIMES = ['09:00','10:00','11:00','13:00','14:00','15:00','16:00','17:00']; 15 const DE_MON = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember']; 16 const DE_DAYS = ['Mo','Di','Mi','Do','Fr','Sa','So']; 17 const DE_WLONG = ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag']; 18 const DE_MSHORT= ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez']; 19 20 const calDays = React.useMemo(() => { 21 const first = new Date(viewYear, viewMonth, 1); 22 let dow = first.getDay() - 1; if (dow < 0) dow = 6; // Mon=0 23 const days = []; 24 for (let i = 0; i < dow; i++) 25 days.push({ d: new Date(viewYear, viewMonth, i - dow + 1), other: true }); 26 const inMonth = new Date(viewYear, viewMonth + 1, 0).getDate(); 27 for (let i = 1; i <= inMonth; i++) 28 days.push({ d: new Date(viewYear, viewMonth, i), other: false }); 29 let n = 1; 30 while (days.length % 7 !== 0) 31 days.push({ d: new Date(viewYear, viewMonth + 1, n++), other: true }); 32 return days; 33 }, [viewYear, viewMonth]); 34 35 const isAvail = (d) => { const w = d.getDay(); return w !== 0 && w !== 6 && d > today; }; 36 const sameDay = (a, b) => a && b && a.toDateString() === b.toDateString(); 37 const fmtLong = (d) => `${DE_WLONG[d.getDay()]}, ${d.getDate()}. ${DE_MSHORT[d.getMonth()]} ${d.getFullYear()}`; 38 const canPrev = viewYear > today.getFullYear() || (viewYear === today.getFullYear() && viewMonth > today.getMonth()); 39 40 const prevM = () => { if (viewMonth === 0) { setViewMonth(11); setViewYear(y=>y-1); } else setViewMonth(m=>m-1); }; 41 const nextM = () => { if (viewMonth === 11) { setViewMonth(0); setViewYear(y=>y+1); } else setViewMonth(m=>m+1); }; 42 43 const handleSend = () => { 44 if (!selDate || !selTime || !name || !email) return; 45 const ds = fmtLong(selDate); 46 const subj = encodeURIComponent(`Videocall-Anfrage: ${ds} um ${selTime} Uhr`); 47 const body = encodeURIComponent( 48 `Hallo,\n\nich möchte gerne einen 15-minütigen Beratungs-Videocall vereinbaren.\n\nWunschtermin: ${ds} um ${selTime} Uhr\nName: ${name}\nE-Mail: ${email}\n\nBitte bestätigt mir den Termin.\n\nViele GrüÃe,\n${name}` 49 ); 50 window.location.href = `mailto:[email protected]?subject=${subj}&body=${body}`; 51 setSent(true); 52 }; 53 54 const navBtn = { 55 background: 'none', border: `1px solid ${T.line}`, 56 width: 32, height: 32, cursor: 'pointer', 57 display: 'flex', alignItems: 'center', justifyContent: 'center', 58 fontSize: 18, color: T.ink, borderRadius: 2, lineHeight: 1 59 }; 60 61 return ( 62 <div style={{ 63 position: 'fixed', inset: 0, zIndex: 500, 64 background: 'rgba(10,10,14,0.78)', backdropFilter: 'blur(8px)', 65 display: 'flex', alignItems: 'center', justifyContent: 'center', 66 padding: 20, overflowY: 'auto' 67 }} onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}> 68 69 <div style={{ background: T.bg, width: '100%', maxWidth: 620, border: `1px solid ${T.line}` }}> 70 71 {/* Modal-Header */} 72 <div style={{ background: T.ink, color: '#fff', padding: '18px 24px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> 73 <div> 74 <div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 11, letterSpacing: '0.22em', color: 'rgba(255,255,255,0.45)', marginBottom: 4 }}>// VIDEOCALL BUCHEN</div> 75 <div style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 20, fontWeight: 600 }}>15 Min · Beratungsgespräch</div> 76 </div> 77 <button onClick={onClose} style={{ background: 'none', border: 'none', color: 'rgba(255,255,255,0.55)', fontSize: 22, cursor: 'pointer', lineHeight: 1, padding: 4 }}>â</button> 78 </div> 79 80 {sent ? ( 81 <div style={{ padding: '48px 28px', textAlign: 'center' }}> 82 <div style={{ fontSize: 52, color: T.accent }}>â</div> 83 <h3 style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 24, fontWeight: 600, margin: '14px 0 8px' }}>Anfrage gesendet</h3> 84 <p style={{ color: T.ink3, fontSize: 15, lineHeight: 1.6, maxWidth: 380, margin: '0 auto 24px' }}> 85 Dein Mail-Programm hat sich mit der vorbereiteten Anfrage geöffnet.<br/> 86 Wir bestätigen Deinen Wunschtermin innerhalb von 24 h. 87 </p> 88 <button onClick={onClose} style={{ padding: '12px 28px', background: T.ink, color: '#fff', border: 'none', fontSize: 15, fontWeight: 600, cursor: 'pointer' }}>SchlieÃen</button> 89 </div> 90 ) : ( 91 <div style={{ padding: 24 }}> 92 93 {/* ââ Kalender ââ */} 94 <div style={{ background: T.surface, border: `1px solid ${T.line}`, marginBottom: 20 }}> 95 96 {/* Monat-Navigation */} 97 <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', borderBottom: `1px solid ${T.line}` }}> 98 <button onClick={prevM} disabled={!canPrev} style={{ ...navBtn, opacity: canPrev ? 1 : 0.25 }}>â¹</button>
99 <span style={{ fontFamily: 'Space Grotesk, sans-serif', fontWeight: 600, fontSize: 16 }}> 100 {DE_MON[viewMonth]} {viewYear} 101 </span> 102 <button onClick={nextM} style={navBtn}>âº</button> 103 </div> 104 105 {/* Wochentag-Kopf */} 106 <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', padding: '8px 12px 2px' }}> 107 {DE_DAYS.map(d => ( 108 <div key={d} style={{ textAlign: 'center', fontSize: 11, fontFamily: 'JetBrains Mono, monospace', letterSpacing: '0.1em', color: T.ink3, paddingBottom: 4 }}>{d}</div> 109 ))} 110 </div> 111 112 {/* Tage-Grid */} 113 <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', padding: '2px 12px 12px', gap: 3 }}> 114 {calDays.map((item, i) => { 115 const avail = !item.other && isAvail(item.d); 116 const sel = sameDay(item.d, selDate); 117 const tod = sameDay(item.d, today); 118 return ( 119 <button key={i} type="button" disabled={!avail} 120 onClick={() => { if (avail) { setSelDate(item.d); setSelTime(''); }}} 121 style={{ 122 padding: '7px 2px', textAlign: 'center', 123 fontFamily: 'inherit', fontSize: 14, 124 border: tod && !sel ? `1px solid ${T.accent}` : '1px solid transparent', 125 borderRadius: 3, 126 background: sel ? T.accent : avail ? T.bg : 'transparent', 127 color: sel ? T.accentInk : item.other ? T.line : avail ? T.ink : T.ink3, 128 cursor: avail ? 'pointer' : 'default', 129 fontWeight: sel ? 700 : 400, 130 opacity: item.other ? 0.25 : 1, 131 transition: 'background .12s, color .12s', 132 boxShadow: avail && !sel ? 'inset 0 0 0 1px rgba(0,0,0,0.06)' : 'none' 133 }}> 134 {item.d.getDate()} 135 </button> 136 ); 137 })} 138 </div> 139 </div> 140 141 {/* ââ Uhrzeit ââ */} 142 {selDate && ( 143 <div style={{ marginBottom: 18 }}> 144 <div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 11, letterSpacing: '0.18em', color: T.ink3, marginBottom: 8 }}> 145 UHRZEIT · {fmtLong(selDate).toUpperCase()} 146 </div> 147 <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}> 148 {TIMES.map(t => ( 149 <button key={t} type="button" onClick={() => setSelTime(t)} style={{ 150 padding: '7px 14px', fontSize: 13, fontFamily: 'JetBrains Mono, monospace', 151 border: `1px solid ${selTime === t ? T.ink : T.line}`, 152 background: selTime === t ? T.ink : 'transparent', 153 color: selTime === t ? '#fff' : T.ink2, 154 cursor: 'pointer', transition: 'all .12s', borderRadius: 2 155 }}>{t} Uhr</button> 156 ))} 157 </div> 158 </div> 159 )} 160 161 {/* ââ Kontaktfelder ââ */} 162 {selTime && ( 163 <> 164 <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 14 }}> 165 {[ 166 { label: 'DEIN NAME', ph: 'Max Mustermann', val: name, set: setName, type: 'text' }, 167 { label: 'DEINE E-MAIL', ph: '[email protected]', val: email, set: setEmail, type: 'email' } 168 ].map(f => ( 169 <div key={f.label}> 170 <div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 11, letterSpacing: '0.15em', color: T.ink3, marginBottom: 6 }}>{f.label}</div> 171 <input type={f.type} placeholder={f.ph} value={f.val} onChange={e => f.set(e.target.value)} 172 style={{ width: '100%', border: `1px solid ${T.line}`, padding: '9px 12px', fontSize: 14, color: T.ink, background: T.bg, fontFamily: 'inherit', outline: 'none', boxSizing: 'border-box' }} 173 onFocus={e => e.target.style.borderColor = T.ink} 174 onBlur={e => e.target.style.borderColor = T.line} /> 175 </div> 176 ))} 177 </div> 178 <button onClick={handleSend} disabled={!name || !email} 179 style={{ 180 width: '100%', padding: '13px', fontSize: 15, fontWeight: 600, 181 background: name && email ? T.accent : T.line, 182 color: name && email ? T.accentInk : T.ink3, 183 border: 'none', cursor: name && email ? 'pointer' : 'default', 184 display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, 185 transition: 'background .2s' 186 }}> 187 Termin anfragen â 188 </button> 189 <p style={{ fontSize: 11, color: T.ink3, marginTop: 10, textAlign: 'center', fontFamily: 'JetBrains Mono, monospace', letterSpacing: '0.06em', lineHeight: 1.5 }}> 190 Dein Mail-Programm öffnet sich · Anfrage geht an [email protected] 191 </p> 192 </> 193 )} 194 195 {/* Hinweis wenn noch kein Tag gewählt */} 196 {!selDate && ( 197 <p style={{ textAlign: 'center', fontSize: 13, color: T.ink3, fontFamily: 'JetBrains Mono, monospace', letterSpacing: '0.08em', marginTop: 4 }}> 198 â Wähle einen Wunschtermin (Mo â Fr) 199 </p> 200 )} 201 </div> 202 )} 203 </div> 204 </div> 205 ); 206} 207 208function PageKontakt({ onNav }) { 209 const T = window.useTheme(); 210 const [selected, setSelected] = React.useState(new Set()); 211 const [calOpen, setCalOpen] = React.useState(false); 212 const toggleInteresse = (t) => setSelected(prev => { 213 const s = new Set(prev); 214 s.has(t) ? s.delete(t) : s.add(t); 215 return s; 216 }); 217 218 return ( 219 <React.Fragment> 220 {calOpen && <VideocallModal T={T} onClose={() => setCalOpen(false)} />} 221 <window.PageHeader label="KONTAKT" dark 222 title={<>Drei Wege,<br />ins <span style={{ color: T.accent }}>Gespräch</span> zu kommen.</>} 223 subtitle="Du musst Dich nicht festlegen â schreib uns, ruf an oder buch einen Videocall. Wir freuen uns über jede Anfrage." /> 224 225 {/* Drei Kanäle */} 226 <window.Section pad="clamp(48px, 6vw, 72px)"> 227 <div className="stack-mobile" style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}> 228 {[ 229 { icon: 'â', code: '// TEL', titel: 'Anrufen', sub: '+49 6104 789 40 51', meta: 'Mo â Fr · 9:00 â 17:00 Uhr', cta: 'Jetzt anrufen' }, 230 { icon: 'â', code: '// MAIL', titel: 'Mailen', sub: '[email protected]', meta: 'Antwort innerhalb 24 h', cta: 'Mail schreiben' }, 231 { icon: 'â', code: '// CALL', titel: 'Videocall', sub: '15 Min Beratungsgespräch', meta: 'Direkt im Kalender buchen', cta: 'Termin buchen', highlight: true }]. 232 map((k, i) => 233 <div key={i}
233 className="card-hover" style={{ 234 padding: 36, 235 background: k.highlight ? T.ink : T.surface, 236 color: k.highlight ? '#fff' : T.ink, 237 border: `1px solid ${k.highlight ? T.ink : T.line}`, 238 display: 'flex', flexDirection: 'column', gap: 20 239 }}> 240 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> 241 <div style={{ 242 width: 56, height: 56, background: k.highlight ? T.accent : '#ece9e2', 243 color: k.highlight ? T.accentInk : T.ink, 244 fontSize: 28, display: 'flex', alignItems: 'center', justifyContent: 'center' 245 }}>{k.icon}</div> 246 <div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 22, letterSpacing: '0.18em', color: k.highlight ? 'rgba(255,255,255,0.45)' : T.ink3 }}> 247 {k.code} 248 </div> 249 </div> 250 <div> 251 <div style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 32, fontWeight: 600, letterSpacing: '-0.02em' }}> 252 {k.titel} 253 </div> 254 <div style={{ fontSize: 22, marginTop: 8, fontWeight: 500 }}> 255 {k.sub} 256 </div> 257 <div style={{ fontSize: 22, marginTop: 6, color: k.highlight ? 'rgba(255,255,255,0.6)' : T.ink3 }}> 258 {k.meta} 259 </div> 260 </div> 261 {k.highlight ? ( 262 <button onClick={() => setCalOpen(true)} style={{ 263 marginTop: 'auto', cursor: 'pointer', 264 display: 'flex', alignItems: 'center', justifyContent: 'space-between', 265 padding: '14px 18px', width: '100%', 266 background: T.accent, color: T.accentInk, 267 border: 'none', fontSize: 22, fontWeight: 600, fontFamily: 'inherit' 268 }}>{k.cta} <span className="cta-arrow">â</span></button> 269 ) : ( 270 <a href={i === 0 ? 'tel:+4961047894051' : 'mailto:[email protected]'} style={{ 271 marginTop: 'auto', 272 display: 'flex', alignItems: 'center', justifyContent: 'space-between', 273 padding: '14px 18px', 274 background: 'transparent', color: T.ink, 275 border: `1px solid ${T.ink}`, 276 fontSize: 22, fontWeight: 600, textDecoration: 'none' 277 }}>{k.cta} <span className="cta-arrow">â</span></a> 278 )} 279 </div> 280 )} 281 </div> 282 </window.Section> 283 284 {/* Anfrage-Formular */} 285 <window.Section pad="clamp(32px, 4vw, 56px)"> 286 <div className="stack-mobile" style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 56 }}> 287 <div style={{ border: `1px solid ${T.line}`, background: T.surface, padding: 'clamp(28px, 4vw, 48px)' }}> 288 <window.MonoLabel>ANFRAGE</window.MonoLabel> 289 <h2 style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 'clamp(28px, 3vw, 36px)', fontWeight: 600, margin: '0 0 32px', letterSpacing: '-0.02em' }}> 290 Schreib uns direkt. 291 </h2> 292 293 {[ 294 { l: 'Name', p: 'Max Mustermann' }, 295 { l: 'E-Mail', p: '[email protected]' }, 296 { l: 'Telefon (optional)', p: '+49 â¦' }]. 297 map((f, i) => 298 <div key={i} style={{ marginBottom: 22 }}> 299 <div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 22, color: T.ink3, letterSpacing: '0.12em', marginBottom: 8 }}> 300 {String(i + 1).padStart(2, '0')} · {f.l.toUpperCase()} 301 </div> 302 <input placeholder={f.p} style={{ 303 width: '100%', border: `1px solid ${T.line}`, 304 padding: '14px 16px', fontSize: 22, 305 color: T.ink, background: T.bg, fontFamily: 'inherit', 306 outline: 'none', transition: 'border .15s' 307 }} 308 onFocus={(e) => e.target.style.borderColor = T.ink} 309 onBlur={(e) => e.target.style.borderColor = T.line} /> 310 311 </div> 312 )} 313 314 <div style={{ marginBottom: 22 }}> 315 <div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 22, color: T.ink3, letterSpacing: '0.12em', marginBottom: 12 }}> 316 04 · INTERESSE 317 </div> 318 <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}> 319 {['Teil 1 · Praxis', 'Teil 2 · Theorie', 'Teil 3 · BWL', 'Teil 4 · AdA', 'Komplettpaket', 'Beratung', 'BAföG-Hilfe'].map((t, i) => { 320 const active = selected.has(t); 321 return <button key={i} onClick={() => toggleInteresse(t)} style={{ 322 padding: '9px 14px', fontSize: 22, 323 border: `1px solid ${active ? T.ink : T.line}`, 324 background: active ? T.ink : 'transparent', 325 color: active ? (T.bg || '#fff') : T.ink2, 326 fontFamily: 'inherit', cursor: 'pointer', 327 transition: 'all .18s'
328 }}>{t}</button>; 329 })} 330 </div> 331 </div> 332 333 <div style={{ marginBottom: 32 }}> 334 <div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 22, color: T.ink3, letterSpacing: '0.12em', marginBottom: 8 }}> 335 05 · NACHRICHT 336 </div> 337 <textarea placeholder="Erzähl uns, wo Du gerade stehst und welche Teile Dich interessieren â¦" style={{ 338 width: '100%', border: `1px solid ${T.line}`, minHeight: 120, 339 fontSize: 22, color: T.ink, fontFamily: 'inherit', 340 background: T.bg, outline: 'none', resize: 'vertical', padding: "1px" 341 }} /> 342 </div> 343 344 <window.Btn variant="primary">Anfrage senden</window.Btn> 345 </div> 346 347 <div> 348 <div style={{ marginBottom: 32 }}> 349 <window.MonoLabel>STANDORT</window.MonoLabel> 350 <div style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 28, fontWeight: 600, lineHeight: 1.3, letterSpacing: '-0.01em' }}> 351 SDS Soltner Dental Schulung<br /> 352 Bgm.-Mahr-Str. 32<br /> 353 63179 Obertshausen 354 </div> 355 <div style={{ fontSize: 22, color: T.ink3, marginTop: 14, fontFamily: 'JetBrains Mono, monospace' }}> 356 50.0609° N, 8.8242° E · 15 Min ab Frankfurt Hbf 357 </div> 358 </div> 359 360 <div style={{ position: 'relative', aspectRatio: '4/3', width: '100%', border: `1px solid ${T.line}`, overflow: 'hidden' }}> 361 <iframe 362 title="Karte · SDS Meisterschule, Obertshausen" 363 src="https://www.openstreetmap.org/export/embed.html?bbox=8.809%2C50.053%2C8.840%2C50.069&layer=mapnik&marker=50.06093%2C8.82423" 364 style={{ width: '100%', height: '100%', border: 0, display: 'block', filter: 'grayscale(0.15) contrast(1.02)' }} 365 loading="lazy" 366 referrerPolicy="no-referrer-when-downgrade"></iframe> 367 <a href="https://www.openstreetmap.org/?mlat=50.06093&mlon=8.82423#map=17/50.06093/8.82423" 368 target="_blank" rel="noopener" 369 style={{ 370 position: 'absolute', bottom: 12, right: 12, 371 background: '#fff', color: T.ink, 372 fontFamily: 'JetBrains Mono, monospace', fontSize: 22, 373 padding: '8px 14px', textDecoration: 'none', 374 letterSpacing: '0.04em', fontWeight: 600, 375 boxShadow: '0 4px 12px rgba(0,0,0,0.18)' 376 }}> 377 In Karten öffnen â 378 </a> 379 <div style={{ 380 position: 'absolute', top: 12, left: 12, 381 background: T.accent, color: T.accentInk, 382 fontFamily: 'JetBrains Mono, monospace', fontSize: 22, 383 padding: '6px 12px', letterSpacing: '0.12em', fontWeight: 700 384 }}> 385 OBERTSHAUSEN 386 </div> 387 </div> 388 389 <div style={{ marginTop: 28, padding: 24, background: T.paper, border: `1px solid ${T.line}` }}> 390 <window.MonoLabel>ÃFFNUNGSZEITEN</window.MonoLabel> 391 {[ 392 ['Mo â Do', '9:00 â 17:00'], 393 ['Freitag', '9:00 â 15:00'], 394 ['Sa / So', 'geschlossen']]. 395 map(([l, t], i) => 396 <div key={i} style={{ 397 display: 'flex', justifyContent: 'space-between', 398 fontFamily: 'JetBrains Mono, monospace', fontSize: 22, 399 padding: '8px 0', borderBottom: i < 2 ? `1px dashed ${T.line}` : 'none' 400 }}> 401 <span>{l}</span><span style={{ color: T.ink2 }}>{t}</span> 402 </div> 403 )} 404 </div> 405 </div> 406 </div> 407 </window.Section> 408 </React.Fragment>); 409 410} 411 412Object.assign(window, { PageKontakt });
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.