Files
mivanchenko c3e520aabb
Test backoffice (smb-crm) / test (push) Successful in 1m43s
Rate-limit public bookings per contact; add direct owner contact endpoint
Public booking API now rejects a 6th active booking from the same
customer_contact within 24h (429), stopping one contact from filling
every slot on every resource, while owner-entered manual bookings stay
unaffected.

Add POST /api/contact: client sites can reach their own owner's inbox
directly (via their existing login email) for general inquiries,
separate from the agency's leads/Telegram pipeline (n8n/lead-intake.json),
which stays reserved for actual prospects contacting the agency itself.
Paris Barber Shop's contact form and Rückruf widget now point here; the
Rückruf floating widget itself has been removed from the site.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 04:47:06 +02:00

409 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Paris Barber Shop — Nürnberg | 2 Filialen | Termin online buchen</title>
<meta name="description" content="Paris Barber Shop in Nürnberg — zwei Filialen (Röthenbach Center &amp; Hafenstraße). Herrenschnitt, Bartpflege und mehr. Jetzt online einen Termin buchen." />
<link rel="icon" type="image/png" href="logo.png" />
<link rel="apple-touch-icon" href="logo.png" />
<meta property="og:title" content="Paris Barber Shop — Nürnberg" />
<meta property="og:description" content="Zwei Filialen in Nürnberg. Herrenschnitt, Bartpflege und mehr — jetzt online einen Termin buchen." />
<meta property="og:image" content="logo.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #14110f;
--bg-2: #1d1916;
--card: #221d19;
--ink: #f4efe9;
--muted: #b3a89c;
--gold: #c8a05a;
--gold-2: #e0c184;
--line: #342d27;
--radius: 14px;
--shadow: 0 18px 50px rgba(0,0,0,.45);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
background: var(--bg); color: var(--ink);
line-height: 1.65; -webkit-font-smoothing: antialiased;
}
h1, h2, h3, .display { font-family: 'Bebas Neue', 'Inter', sans-serif; letter-spacing: .5px; line-height: 1.05; }
a { color: inherit; text-decoration: none; }
.wrap { width: min(1120px, 92vw); margin-inline: auto; }
section { padding: 92px 0; }
.eyebrow { color: var(--gold); font-weight: 600; letter-spacing: 3px; text-transform: uppercase; font-size: .72rem; }
/* nav */
header.nav { position: sticky; top: 0; z-index: 50; background: rgba(20,17,15,.82); backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); }
.nav-inner { display: flex; align-items: center; justify-content: space-between; height: 68px; }
.brand { font-family: 'Bebas Neue', sans-serif; font-size: 1.5rem; letter-spacing: 1.5px; display: flex; align-items: center; gap: 10px; }
.brand span { color: var(--gold); }
.brand img { height: 44px; width: 44px; border-radius: 50%; display: block; }
footer .brand img { height: 52px; width: 52px; }
nav ul { display: flex; gap: 30px; list-style: none; align-items: center; }
nav a { color: var(--muted); font-size: .92rem; font-weight: 500; transition: color .2s; }
nav a:hover { color: var(--ink); }
.btn {
display: inline-block; background: linear-gradient(180deg, var(--gold-2), var(--gold));
color: #1a1410; font-weight: 700; padding: 12px 22px; border-radius: 999px;
border: 0; cursor: pointer; font-size: .95rem; transition: transform .15s, box-shadow .2s;
box-shadow: 0 8px 22px rgba(200,160,90,.28);
}
.btn:hover { transform: translateY(-2px); box-shadow: 0 12px 28px rgba(200,160,90,.4); }
.btn.ghost { background: transparent; color: var(--ink); border: 1px solid var(--line); box-shadow: none; }
.nav-toggle { display: none; background: none; border: 0; color: var(--ink); font-size: 1.6rem; cursor: pointer; }
/* hero */
.hero { position: relative; background:
linear-gradient(rgba(15,12,10,.72), rgba(15,12,10,.86)),
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60'%3E%3Crect width='60' height='60' fill='%231a1512'/%3E%3Cpath d='M0 59h60M0 29h60' stroke='%23241d18' stroke-width='1'/%3E%3C/svg%3E");
border-bottom: 1px solid var(--line); }
.hero-inner { padding: 96px 0 104px; max-width: 720px; }
.hero h1 { font-size: clamp(2.8rem, 7.5vw, 5rem); margin: 14px 0 6px; }
.hero h1 em { color: var(--gold); font-style: normal; }
.hero p { color: var(--muted); font-size: 1.18rem; max-width: 540px; margin: 18px 0 30px; }
.hero-cta { display: flex; gap: 14px; flex-wrap: wrap; }
.hero-meta { margin-top: 34px; display: flex; gap: 28px; flex-wrap: wrap; color: var(--muted); font-size: .9rem; }
.hero-meta b { color: var(--ink); }
/* location switcher */
.loc-switch { background: var(--bg-2); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.loc-tabs { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 28px; }
.loc-tab {
background: var(--card); border: 1px solid var(--line); border-radius: 999px;
padding: 11px 20px; font-size: .92rem; font-weight: 600; color: var(--muted);
cursor: pointer; transition: .15s;
}
.loc-tab.active { background: var(--gold); border-color: var(--gold); color: #1a1410; }
.loc-tab:hover:not(.active) { border-color: var(--gold); color: var(--ink); }
.loc-card { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 30px; box-shadow: var(--shadow); display: grid; grid-template-columns: 1.2fr 1fr; gap: 30px; }
.loc-card h3 { font-size: 1.9rem; margin-bottom: 4px; }
.loc-rating { color: var(--gold-2); font-weight: 600; font-size: .95rem; margin-bottom: 16px; }
.loc-rating span { color: var(--muted); font-weight: 400; }
.loc-row { display: flex; gap: 10px; margin-bottom: 10px; color: var(--muted); font-size: .95rem; }
.loc-row b { color: var(--ink); font-weight: 500; }
.loc-status { display: inline-flex; align-items: center; gap: 7px; color: #7fd18e; font-weight: 600; font-size: .88rem; margin-top: 4px; }
.loc-status .dot { width: 8px; height: 8px; border-radius: 50%; background: #4caf7d; box-shadow: 0 0 0 3px rgba(76,175,125,.2); }
.loc-hours td { padding: 7px 0; border-bottom: 1px solid var(--line); color: var(--muted); font-size: .88rem; }
.loc-hours td:last-child { text-align: right; color: var(--ink); }
.loc-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px; }
/* services */
.sec-head { max-width: 620px; margin-bottom: 46px; }
.sec-head h2 { font-size: clamp(2.2rem, 5vw, 3.2rem); margin-top: 8px; }
.sec-head p { color: var(--muted); margin-top: 10px; }
.grid { display: grid; gap: 18px; }
.services { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.svc { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 26px; transition: transform .18s, border-color .2s; }
.svc:hover { transform: translateY(-4px); border-color: var(--gold); }
.svc .ic { font-size: 1.8rem; }
.svc h3 { font-size: 1.5rem; margin: 14px 0 6px; }
.svc p { color: var(--muted); font-size: .92rem; min-height: 42px; }
.svc .price { color: var(--gold-2); font-weight: 700; font-size: .98rem; margin-top: 12px; font-family: 'Inter'; }
/* booking */
.booking { border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.booking ul.points { list-style: none; display: grid; gap: 14px; margin: 22px 0 28px; }
.booking ul.points li { padding-left: 30px; position: relative; color: var(--muted); }
.booking ul.points li::before { content: "✓"; position: absolute; left: 0; color: var(--gold); font-weight: 700; }
.booking-embed { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 8px; box-shadow: var(--shadow); max-width: 640px; margin: 0 auto; }
.booking-embed iframe { width: 100%; height: 900px; min-height: 0; border: 0; border-radius: 10px; display: block; background: #1a1611; }
@media (max-width: 800px) { .booking-embed iframe { height: 1050px; } }
/* contact */
.contact { background: var(--bg-2); border-top: 1px solid var(--line); }
form.lead { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 28px; max-width: 560px; }
.field { margin-bottom: 16px; }
.field label { display: block; font-size: .85rem; color: var(--muted); margin-bottom: 6px; }
.field input, .field textarea, .field select {
width: 100%; background: #1a1611; border: 1px solid var(--line); border-radius: 9px;
color: var(--ink); padding: 12px 14px; font: inherit; font-size: .95rem;
}
.field input:focus, .field textarea:focus, .field select:focus { outline: 2px solid var(--gold); border-color: var(--gold); }
.form-note { font-size: .78rem; color: var(--muted); margin-top: 10px; }
.toast { display: none; margin-top: 14px; padding: 12px 14px; border-radius: 9px; background: #1e2a1c; border: 1px solid #2f5a2c; color: #bfe6b6; font-size: .9rem; }
/* footer */
footer { padding: 54px 0 40px; border-top: 1px solid var(--line); color: var(--muted); font-size: .9rem; }
.foot-grid { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 24px; }
footer .brand { color: var(--ink); }
@media (max-width: 820px) {
nav ul { display: none; }
.nav-toggle { display: block; }
nav.open ul { display: flex; position: absolute; inset: 68px 0 auto 0; flex-direction: column; background: var(--bg-2); padding: 18px 6vw; gap: 18px; border-bottom: 1px solid var(--line); }
.loc-card { grid-template-columns: 1fr; }
section { padding: 64px 0; }
}
</style>
</head>
<body>
<header class="nav">
<div class="wrap nav-inner">
<a href="#top" class="brand"><img src="logo.png" alt="Paris Barber Shop" width="44" height="44" />PARIS <span>BARBER SHOP</span></a>
<nav id="nav">
<ul>
<li><a href="#filialen">Filialen</a></li>
<li><a href="#leistungen">Leistungen</a></li>
<li><a href="#buchen">Termin buchen</a></li>
<li><a href="#kontakt">Kontakt</a></li>
<li><a href="#buchen" class="btn">Termin buchen</a></li>
</ul>
</nav>
<button class="nav-toggle" aria-label="Menü" onclick="document.getElementById('nav').classList.toggle('open')"></button>
</div>
</header>
<main id="top">
<!-- HERO -->
<section class="hero" id="hero">
<div class="wrap hero-inner">
<span class="eyebrow">Herrenfriseur · 2 Filialen in Nürnberg</span>
<h1>Sauberer Schnitt.<br><em>Zwei Adressen.</em></h1>
<p>Herrenschnitt, Bartpflege und mehr — in unseren Filialen im Röthenbach Center und an der Hafenstraße. Termin online buchen, Filiale frei wählbar.</p>
<div class="hero-cta">
<a href="#buchen" class="btn">Termin online buchen</a>
<a href="#filialen" class="btn ghost">Filialen &amp; Öffnungszeiten</a>
</div>
<div class="hero-meta">
<span><b>4,24,4</b> · 500+ Bewertungen</span>
<span>✂ Herrenfriseur</span>
<span>📍 Nürnberg-West</span>
</div>
</div>
</section>
<!-- LOCATION SWITCHER -->
<section class="loc-switch" id="filialen">
<div class="wrap">
<div class="sec-head" style="margin-bottom:28px;">
<span class="eyebrow">Filialen</span>
<h2>Welche Filiale passt für Sie?</h2>
<p>Beide Filialen sind eigenständig buchbar — wählen Sie unten aus.</p>
</div>
<div class="loc-tabs" id="locTabs" role="tablist">
<button class="loc-tab active" type="button" data-loc="roethenbach">Röthenbach Center</button>
<button class="loc-tab" type="button" data-loc="hafenstrasse">Hafenstraße</button>
</div>
<div class="loc-card" id="locCard"><!-- filled by JS --></div>
</div>
</section>
<!-- SERVICES -->
<section id="leistungen">
<div class="wrap">
<div class="sec-head">
<span class="eyebrow">Leistungen</span>
<h2>Was wir für Sie tun</h2>
<p>In beiden Filialen verfügbar — Preise und Details erfahren Sie bei der Buchung oder im Studio.</p>
</div>
<div class="grid services">
<article class="svc"><div class="ic">✂️</div><h3>Herrenschnitt</h3><p>Waschen, Schnitt, Styling — individuell auf Ihren Typ abgestimmt.</p><div class="price">30 Min</div></article>
<article class="svc"><div class="ic">🧔</div><h3>Bartpflege</h3><p>Konturieren, trimmen und pflegen für den perfekten Bart.</p><div class="price">20 Min</div></article>
</div>
</div>
</section>
<!-- BOOKING -->
<section class="booking" id="buchen">
<div class="wrap">
<div class="sec-head" style="margin-bottom:28px;">
<span class="eyebrow">Termin buchen</span>
<h2>Wählen Sie Filiale, Leistung &amp; Zeit.</h2>
<p>Buchen Sie direkt hier rund um die Uhr — sofortige Bestätigung, kein Anruf nötig. Die Filialauswahl finden Sie im Buchungsfenster unter „Mitarbeiter&quot;.</p>
</div>
<div class="booking-embed">
<iframe id="booking-frame" src="https://onboard.mivanchenko.de/book/paris-barbershop?color=%23c8a05a"
title="Online-Terminbuchung" loading="lazy"></iframe>
</div>
</div>
</section>
<!-- CONTACT / LEAD FORM -->
<section class="contact" id="kontakt">
<div class="wrap">
<div class="sec-head">
<span class="eyebrow">Kontakt</span>
<h2>Frage stellen</h2>
<p>Schreiben Sie uns kurz — wir melden uns am selben Tag.</p>
</div>
<form class="lead" onsubmit="return submitLead(event, this)">
<input type="hidden" name="client_id" value="C-0004" />
<input type="hidden" name="source" value="paris-barbershop-site" />
<div class="field"><label for="a-name">Name</label><input id="a-name" name="name" type="text" placeholder="Ihr Name" required /></div>
<div class="field"><label for="a-contact">E-Mail oder Telefon</label><input id="a-contact" name="contact" type="text" placeholder="name@example.de" required /></div>
<div class="field"><label for="a-loc">Bevorzugte Filiale</label>
<select id="a-loc" name="service"><option>Röthenbach Center</option><option>Hafenstraße</option><option>Egal</option></select>
</div>
<div class="field"><label for="a-msg">Nachricht (optional)</label><textarea id="a-msg" name="message" rows="3" placeholder="Wunschtermin, Frage, …"></textarea></div>
<button class="btn" type="submit">Anfrage senden</button>
<div class="toast">✓ Danke! Ihre Anfrage ist eingegangen — wir melden uns.</div>
<p class="form-note">Mit dem Absenden stimmen Sie der Verarbeitung Ihrer Angaben zur Kontaktaufnahme zu.</p>
</form>
</div>
</section>
</main>
<footer>
<div class="wrap foot-grid">
<div>
<div class="brand"><img src="logo.png" alt="Paris Barber Shop" width="52" height="52" />PARIS <span style="color:var(--gold)">BARBER SHOP</span></div>
<p style="margin-top:8px;">Herrenfriseur · Nürnberg</p>
</div>
<div>
<p><b style="color:var(--ink)">Röthenbach Center</b></p>
<p>Dombühler Str. 9 · 0911 47885170</p>
</div>
<div>
<p><b style="color:var(--ink)">Hafenstraße</b></p>
<p>Hafenstraße 12 · 0911 25387841</p>
</div>
</div>
<div class="wrap" style="margin-top:30px;opacity:.6;font-size:.8rem;">© 2026 Paris Barber Shop · Impressum · Datenschutz</div>
</footer>
<script>
// weekly: index 0=Sunday..6=Saturday (JS Date#getDay convention), minutes
// since midnight, or null for a closed day -- matches the real hours
// configured in the booking backend (Mon-Sat 09:00-20:00, Sun closed).
const OPEN_MIN = 9 * 60, CLOSE_MIN = 20 * 60;
const WEEKLY_SCHEDULE = [null, [OPEN_MIN, CLOSE_MIN], [OPEN_MIN, CLOSE_MIN], [OPEN_MIN, CLOSE_MIN],
[OPEN_MIN, CLOSE_MIN], [OPEN_MIN, CLOSE_MIN], [OPEN_MIN, CLOSE_MIN]];
const LOCATIONS = {
roethenbach: {
name: "Paris Barber Shop — Röthenbach Center",
rating: "4,2", reviews: "277",
address: "Dombühler Str. 9, 90449 Nürnberg-West",
note: "Befindet sich in: Röthenbach Center",
phone: "0911 47885170",
phoneHref: "tel:+4991147885170",
site: "hair.center.ivof.com",
hours: [["Montag Samstag", "09:00 20:00"], ["Sonntag", "geschlossen"]],
schedule: WEEKLY_SCHEDULE,
},
hafenstrasse: {
name: "Paris Barber Shop II — Hafenstraße",
rating: "4,4", reviews: "226",
address: "Hafenstraße 12, 90451 Nürnberg-West",
note: "",
phone: "0911 25387841",
phoneHref: "tel:+4991125387841",
site: "barber.de.ivof.com",
hours: [["Montag Samstag", "09:00 20:00"], ["Sonntag", "geschlossen"]],
schedule: WEEKLY_SCHEDULE,
},
};
function fmtMin(min) {
return String(Math.floor(min / 60)).padStart(2, '0') + ':' + String(min % 60).padStart(2, '0');
}
// Real open/closed status computed against the shop's own timezone
// (Europe/Berlin), not the visitor's -- a Google-Maps-style "Geöffnet ·
// schließt um 20:00" badge is misleading if it's just hardcoded text that
// never changes, e.g. still claiming "open" at 3am.
function computeStatus(schedule) {
const berlinNow = new Date(new Date().toLocaleString('en-US', { timeZone: 'Europe/Berlin' }));
const day = berlinNow.getDay();
const today = schedule[day];
const nowMin = berlinNow.getHours() * 60 + berlinNow.getMinutes();
if (today) {
if (nowMin >= today[0] && nowMin < today[1]) {
return { open: true, text: 'Geöffnet · schließt um ' + fmtMin(today[1]) + ' Uhr' };
}
if (nowMin < today[0]) {
return { open: false, text: 'Geschlossen · öffnet heute um ' + fmtMin(today[0]) + ' Uhr' };
}
}
// Closed all day today, or already past today's closing time -- find
// the next day (starting tomorrow) that has hours at all.
for (let ahead = 1; ahead <= 7; ahead++) {
const next = schedule[(day + ahead) % 7];
if (next) {
const when = ahead === 1 ? 'morgen' : 'in ' + ahead + ' Tagen';
return { open: false, text: 'Geschlossen · öffnet ' + when + ' um ' + fmtMin(next[0]) + ' Uhr' };
}
}
return { open: false, text: 'Geschlossen' };
}
function renderLoc(key) {
const l = LOCATIONS[key];
const mapsUrl = "https://www.google.com/maps/search/?api=1&query=" + encodeURIComponent(l.address);
const hoursRows = l.hours.map(function (h) {
return '<tr><td>' + h[0] + '</td><td>' + h[1] + '</td></tr>';
}).join('');
const status = computeStatus(l.schedule);
document.getElementById('locCard').innerHTML =
'<div>' +
'<h3>' + l.name + '</h3>' +
'<div class="loc-rating">⭐ ' + l.rating + ' <span>(' + l.reviews + ' Bewertungen)</span></div>' +
'<div class="loc-row">📍 <b>' + l.address + '</b></div>' +
(l.note ? '<div class="loc-row" style="margin-left:24px;">' + l.note + '</div>' : '') +
'<div class="loc-row">☎ <a href="' + l.phoneHref + '"><b>' + l.phone + '</b></a></div>' +
'<div class="loc-status" style="color:' + (status.open ? '#7fd18e' : '#c9736f') + ';">' +
'<span class="dot" style="background:' + (status.open ? '#4caf7d' : '#b3564f') + ';"></span> ' + status.text +
'</div>' +
'<div class="loc-actions">' +
'<a class="btn" href="#buchen">Termin in dieser Filiale</a>' +
'<a class="btn ghost" href="' + mapsUrl + '" target="_blank" rel="noopener">Routenplaner</a>' +
'</div>' +
'</div>' +
'<div>' +
'<span class="eyebrow">Öffnungszeiten</span>' +
'<table class="loc-hours" style="width:100%;margin-top:10px;">' + hoursRows + '</table>' +
'</div>';
}
document.getElementById('locTabs').addEventListener('click', function (e) {
const btn = e.target.closest('.loc-tab');
if (!btn) return;
document.querySelectorAll('.loc-tab').forEach(function (t) { t.classList.remove('active'); });
btn.classList.add('active');
renderLoc(btn.getAttribute('data-loc'));
});
renderLoc('roethenbach');
</script>
<script>
// Kontaktformular -> direkt eine E-Mail an den Inhaber (kein CRM-Lead, kein Telegram).
const LEAD_WEBHOOK = 'https://onboard.mivanchenko.de/api/contact';
async function submitLead(e, form) {
e.preventDefault();
const btn = form.querySelector('button[type=submit]');
const toast = form.querySelector('.toast');
const data = Object.fromEntries(new FormData(form).entries());
const old = btn.textContent;
btn.disabled = true; btn.textContent = 'Senden…';
try {
const r = await fetch(LEAD_WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!r.ok) throw new Error(r.status);
toast.textContent = '✓ Danke! Ihre Anfrage ist eingegangen — wir melden uns.';
toast.style.display = 'block';
form.reset();
} catch (err) {
toast.textContent = '⚠ Senden fehlgeschlagen — bitte erneut versuchen oder direkt anrufen.';
toast.style.display = 'block';
} finally {
btn.disabled = false; btn.textContent = old;
}
return false;
}
</script>
</body>
</html>