PageSourceSearch

https://sdswebsite.netlify.app/site/page-kontakt.jsx

js sdswebsite.netlify.app collected 2026-10-03 11:23:01 UTC 22,039 bytes, 412 lines download raw bytes

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.