16 Commits

Author SHA1 Message Date
127faaffaf feat(favicon): use Außenansicht as favicon base
All checks were successful
Deploy Feature Branch to Test / deploy (push) Successful in 24s
2026-05-15 08:43:47 +00:00
c6eda36750 feat(favicon): add favicon and browser icons for Issue #36
All checks were successful
Deploy Feature Branch to Test / deploy (push) Successful in 37s
- favicon.ico (16x16 + 32x32)
- favicon-32x32.png, favicon-16x16.png
- apple-touch-icon.png (180x180)
- site.webmanifest
- Linked in index.php head

Resolves #36
2026-05-15 08:40:16 +00:00
336fbc12a6 Merge pull request 'Fix #18: Accessibility – ARIA-Labels, Focus-Management, Skip-Navigation' (#24) from feature/issue-18-accessibility into main 2026-05-15 10:32:45 +02:00
Claw AI
8b73603293 feat(a11y): ARIA labels, focus management, skip-nav, keyboard nav, contrast fix
All checks were successful
Deploy Feature Branch to Test / deploy (push) Successful in 23s
Accessibility improvements per WCAG 2.1 AA:
- Skip-to-content link (TA-1)
- ARIA landmarks and roles for nav, main, sections, footer (TA-2)
- Accordion keyboard navigation + aria-expanded (TA-3)
- Lightbox focus trap + focus management + dialog role (TA-4)
- Gallery grid items keyboard accessible (TA-5)
- Improved alt texts for all images (TA-6)
- Focus-visible styles for all interactive elements (TA-7)
- Darker --stone color for WCAG AA contrast compliance (TA-8)

Fix #18
2026-05-15 08:32:26 +00:00
d609175b3c Merge pull request 'Fix #19: Remove jQuery dependency, replace with vanilla JS' (#21) from feature/issue-19-remove-jquery-masonry into main 2026-05-15 10:29:56 +02:00
greggy
1fcdca95b7 refactor(js): remove jQuery dependency and replace with vanilla JS
All checks were successful
Deploy Feature Branch to Test / deploy (push) Successful in 24s
- Rewrite haus-schleusingen.js entirely in vanilla JavaScript
- Use IntersectionObserver instead of scroll event for scroll animations
- Replace jQuery slideUp/slideDown with display toggle for accordion
- Replace jQuery fadeIn with CSS opacity transition for form success
- Remove jQuery CDN script tag from haus-schleusingen.html
- Delete unused masonry.pkgd.min.js
- Remove jquery globals from eslint.config.js

Ref #19
2026-05-15 07:57:01 +00:00
88ef7aa6ac Merge pull request 'Fix #34: Kontaktformular E-Mail-Versand via PHP' (#35) from feature/issue-34-contact-form-mail into main 2026-05-15 09:50:43 +02:00
bf53da13be Fix: Scroll to form result after submission (PRG pattern with anchor)
All checks were successful
Deploy Feature Branch to Test / deploy (push) Successful in 23s
2026-05-14 22:38:27 +00:00
2307c379dc Revert to PHP mail() for portability, remove AgentMail API dependency
All checks were successful
Deploy Feature Branch to Test / deploy (push) Successful in 24s
2026-05-14 22:25:24 +00:00
2c6ed749d5 Fix: Use AgentMail API instead of mail(), fix reply_to format
All checks were successful
Deploy Feature Branch to Test / deploy (push) Successful in 24s
2026-05-14 22:20:59 +00:00
c2f2709790 feat(contact): server-side PHP mail handler for contact form
All checks were successful
Deploy Feature Branch to Test / deploy (push) Successful in 24s
Fix #34: E-Mail-Versand via PHP

- PHP POST handler with server-side validation (name, email, message)
- mail() with From/Reply-To set to form email address
- Recipient: mki@kies-media.de
- Honeypot spam protection (hidden field)
- Minimum submit time check (3 seconds)
- Session-based rate limiting (60s between submissions)
- Header injection protection
- Error messages displayed above form
- Success message after successful send
- Form values preserved on validation errors
- Removed client-side mailto: JavaScript logic
- Added CSS for error display and honeypot hiding
2026-05-14 19:12:43 +00:00
69ca8efa47 Merge pull request 'Rename haus-schleusingen.html to index.php' (#33) from feature/rename-to-index-php into main 2026-05-14 20:55:38 +02:00
40001adbce Rename haus-schleusingen.html to index.php
All checks were successful
Deploy Feature Branch to Test / deploy (push) Successful in 24s
2026-05-14 18:53:15 +00:00
158f07e374 Merge pull request 'Fix #27: Mobile Navigation – Hamburger-Menü implementieren' (#32) from feature/issue-27-hamburger-menu into main 2026-05-14 20:46:19 +02:00
76b1ec58c2 style(nav): remove duplicate display property in mobile nav links
All checks were successful
Deploy Feature Branch to Test / deploy (push) Successful in 24s
2026-05-14 17:40:51 +00:00
565c8b304d feat(nav): add hamburger menu for mobile navigation (Fix #27)
All checks were successful
Deploy Feature Branch to Test / deploy (push) Successful in 24s
- Hamburger button with animated X toggle (CSS-only icon)
- Slide-down mobile nav on ≤900px with 44px+ tap targets
- Semi-transparent overlay when menu is open
- Escape key + outside click + link click closes menu
- Auto-close on resize to desktop
- Desktop navigation unchanged
- Pure vanilla JS toggle, no jQuery dependency
2026-05-14 17:40:20 +00:00
10 changed files with 663 additions and 122 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
bilder/favicon/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,10 @@
{
"name": "Haus Schleusingen",
"short_name": "HS",
"icons": [
{ "src": "/bilder/favicon/favicon-32x32.png", "sizes": "32x32", "type": "image/png" },
{ "src": "/bilder/favicon/favicon-16x16.png", "sizes": "16x16", "type": "image/png" }
],
"theme_color": "#1c1917",
"background_color": "#fafaf9"
}

View File

@@ -1,7 +1,55 @@
/* SKIP LINK */
.skip-link {
position: absolute;
left: -9999px;
top: 0;
background: var(--accent);
color: var(--white);
padding: 0.75rem 1.5rem;
font-size: 0.85rem;
font-weight: 500;
z-index: 200;
text-decoration: none;
transition: none;
}
.skip-link:focus {
left: 0;
outline: 2px solid var(--white);
outline-offset: 2px;
}
/* FOCUS VISIBLE */
*:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.lightbox-close:focus-visible {
outline: 2px solid var(--white);
outline-offset: 2px;
}
button:focus-visible,
a:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.grid-item:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.floor-header:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
:root { :root {
--cream: #f5f0e8; --cream: #f5f0e8;
--warm: #e8dfd0; --warm: #e8dfd0;
--stone: #9e9485; --stone: #7a7062;
--dark: #1c1a17; --dark: #1c1a17;
--charcoal: #2e2b26; --charcoal: #2e2b26;
--accent: #8b6914; --accent: #8b6914;
@@ -115,6 +163,84 @@ nav.scrolled .nav-links a:hover {
box-shadow: 0 4px 20px rgb(139 105 20 / 50%); box-shadow: 0 4px 20px rgb(139 105 20 / 50%);
} }
/* HAMBURGER */
.nav-hamburger {
display: none;
width: 44px;
height: 44px;
background: none;
border: none;
cursor: pointer;
position: relative;
z-index: 110;
align-items: center;
justify-content: center;
padding: 0;
}
.nav-hamburger span,
.nav-hamburger span::before,
.nav-hamburger span::after {
display: block;
width: 22px;
height: 2px;
background: var(--white);
border-radius: 1px;
transition:
transform 0.3s ease,
opacity 0.3s ease,
background 0.4s;
position: absolute;
}
.nav-hamburger span::before,
.nav-hamburger span::after {
content: "";
}
.nav-hamburger span::before {
transform: translateY(-7px);
}
.nav-hamburger span::after {
transform: translateY(7px);
}
nav.scrolled .nav-hamburger span,
nav.scrolled .nav-hamburger span::before,
nav.scrolled .nav-hamburger span::after {
background: var(--dark);
}
.nav-hamburger.active span {
background: transparent;
}
.nav-hamburger.active span::before {
transform: rotate(45deg);
background: var(--dark);
}
.nav-hamburger.active span::after {
transform: rotate(-45deg);
background: var(--dark);
}
.nav-mobile-overlay {
display: none;
position: fixed;
inset: 0;
background: rgb(28 26 23 / 50%);
z-index: 90;
opacity: 0;
transition: opacity 0.3s ease;
}
.nav-mobile-overlay.active {
display: block;
opacity: 1;
}
/* HERO */ /* HERO */
.hero { .hero {
position: relative; position: relative;
@@ -913,6 +1039,36 @@ nav.scrolled .nav-links a:hover {
transform: translateY(-1px); transform: translateY(-1px);
} }
.form-errors {
background: #fdf2f2;
border: 1px solid #e8a0a0;
padding: 1rem 1.2rem;
margin-bottom: 1.2rem;
}
.form-errors ul {
margin: 0;
padding: 0 0 0 1.2rem;
list-style: disc;
}
.form-errors li {
font-size: 0.85rem;
color: #9e2c2c;
line-height: 1.6;
}
.hp-field {
position: absolute;
left: -9999px;
top: -9999px;
width: 0;
height: 0;
overflow: hidden;
opacity: 0;
pointer-events: none;
}
.form-success { .form-success {
display: none; display: none;
text-align: center; text-align: center;
@@ -1042,6 +1198,58 @@ footer {
display: none; display: none;
} }
.nav-hamburger {
display: flex;
}
/* Mobile slide-down nav */
nav.mobile-open .nav-links {
display: flex;
flex-direction: column;
position: fixed;
top: 0;
left: 0;
right: 0;
background: rgb(253 252 250 / 98%);
backdrop-filter: blur(12px);
padding: 5rem 1.5rem 2rem;
gap: 0;
z-index: 95;
border-bottom: 1px solid var(--warm);
animation: slideDown 0.3s ease;
}
nav.mobile-open .nav-links a {
color: var(--charcoal);
font-size: 1rem;
padding: 1rem 0;
border-bottom: 1px solid var(--warm);
display: flex;
align-items: center;
min-height: 44px;
}
nav.mobile-open .nav-hamburger span,
nav.mobile-open .nav-hamburger span::before,
nav.mobile-open .nav-hamburger span::after {
background: var(--dark);
}
nav.mobile-open .nav-hamburger.active span {
background: transparent;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.hero-content { .hero-content {
padding: 0 1.5rem 4rem; padding: 0 1.5rem 4rem;
} }

View File

@@ -13,7 +13,6 @@ module.exports = [
sourceType: "script", sourceType: "script",
globals: { globals: {
...globals.browser, ...globals.browser,
...globals.jquery,
}, },
}, },
plugins: { plugins: {

View File

@@ -1,7 +1,136 @@
<?php
session_start();
// --- Helper functions ---
function normalizeContactValue(string $value): string
{
return trim($value);
}
function escapeContactValue(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
function containsHeaderInjection(string $value): bool
{
return (bool) preg_match('/[\r\n]/', $value);
}
// --- Form processing ---
$formErrors = [];
$formSuccess = false;
if (!empty($_SESSION['form_success'])) {
$formSuccess = true;
unset($_SESSION['form_success']);
}
if (!empty($_SESSION['form_errors'])) {
$formErrors = $_SESSION['form_errors'];
unset($_SESSION['form_errors']);
}
if (!empty($_SESSION['form_data'])) {
$formData = $_SESSION['form_data'];
unset($_SESSION['form_data']);
} else {
$formData = ['fname' => '', 'lname' => '', 'email' => '', 'phone' => '', 'interest' => 'Besichtigung anfragen', 'message' => ''];
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Collect and normalize input
$formData['fname'] = normalizeContactValue((string) ($_POST['fname'] ?? ''));
$formData['lname'] = normalizeContactValue((string) ($_POST['lname'] ?? ''));
$formData['email'] = normalizeContactValue((string) ($_POST['email'] ?? ''));
$formData['phone'] = normalizeContactValue((string) ($_POST['phone'] ?? ''));
$formData['interest'] = normalizeContactValue((string) ($_POST['interest'] ?? ''));
$formData['message'] = normalizeContactValue((string) ($_POST['message'] ?? ''));
// Honeypot check hidden field must be empty
$honeypot = normalizeContactValue((string) ($_POST['website'] ?? ''));
if ($honeypot !== '') {
// Bot detected pretend success
header('Location: ' . $_SERVER['REQUEST_URI'] . '#form-result');
$_SESSION['form_success'] = true;
exit;
} else {
// Server-side validation
if ($formData['fname'] === '') {
$formErrors[] = 'Bitte geben Sie Ihren Vornamen an.';
}
if ($formData['lname'] === '') {
$formErrors[] = 'Bitte geben Sie Ihren Nachnamen an.';
}
if ($formData['email'] === '' || !filter_var($formData['email'], FILTER_VALIDATE_EMAIL)) {
$formErrors[] = 'Bitte geben Sie eine gültige E-Mail-Adresse an.';
}
if ($formData['message'] === '') {
$formErrors[] = 'Bitte geben Sie eine Nachricht ein.';
}
// Header injection check
if (containsHeaderInjection($formData['email']) || containsHeaderInjection($formData['fname'] . ' ' . $formData['lname'])) {
$formErrors[] = 'Ungültige Zeichen in den Eingabefeldern.';
}
// Minimum time check form submitted too fast (< 3 seconds)
$formTime = isset($_POST['form_time']) ? (int) $_POST['form_time'] : 0;
if ($formTime > 0 && (time() - $formTime) < 3) {
$formErrors[] = 'Das Formular wurde zu schnell abgeschickt. Bitte versuchen Sie es erneut.';
}
// Session rate limit max 1 submission per 60 seconds
$lastSubmit = $_SESSION['last_contact_submit'] ?? 0;
if ($lastSubmit && (time() - $lastSubmit) < 60) {
$formErrors[] = 'Bitte warten Sie einen Moment vor der nächsten Anfrage.';
}
// Send email if no errors
if (empty($formErrors)) {
$to = 'mki@kies-media.de';
$subject = 'Kontaktanfrage: ' . $formData['interest'];
$body = "Von: {$formData['fname']} {$formData['lname']}\n"
. "E-Mail: {$formData['email']}\n";
if ($formData['phone'] !== '') {
$body .= "Telefon: {$formData['phone']}\n";
}
$body .= "Anliegen: {$formData['interest']}\n\n"
. $formData['message'];
$headers = "From: {$formData['email']}\r\n";
$headers .= "Reply-To: {$formData['email']}\r\n";
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
$headers .= "X-Mailer: PHP/" . phpversion();
$mailSent = mail($to, $subject, $body, $headers);
if ($mailSent) {
$_SESSION['last_contact_submit'] = time();
header('Location: ' . $_SERVER['REQUEST_URI'] . '#form-result');
$_SESSION['form_success'] = true;
exit;
} else {
$formErrors[] = 'Leider konnte die E-Mail nicht gesendet werden. Bitte versuchen Sie es später erneut oder schreiben Sie uns direkt an mki@kies-media.de.';
}
}
}
if (!empty($formErrors)) {
header('Location: ' . $_SERVER['REQUEST_URI'] . '#form-result');
$_SESSION['form_errors'] = $formErrors;
$_SESSION['form_data'] = $formData;
exit;
}
}
?>
<!doctype html> <!doctype html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/png" sizes="32x32" href="/bilder/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/bilder/favicon/favicon-16x16.png">
<link rel="icon" type="image/x-icon" href="/bilder/favicon/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/bilder/favicon/apple-touch-icon.png">
<link rel="manifest" href="/bilder/favicon/site.webmanifest">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Einfamilienhaus mieten Schleusingen | 227 , 6 Zimmer | 1.300 Kaltmiete</title> <title>Einfamilienhaus mieten Schleusingen | 227 , 6 Zimmer | 1.300 Kaltmiete</title>
<meta name="description" content="Einfamilienhaus zur Langzeitmiete in Schleusingen: 227 m² Wohnfläche, 6 Zimmer, 3 Etagen mit Dachterrasse. Kaltmiete 1.300 €. Bahnhofstraße 10, 98553 Schleusingen. Ab sofort verfügbar." /> <meta name="description" content="Einfamilienhaus zur Langzeitmiete in Schleusingen: 227 m² Wohnfläche, 6 Zimmer, 3 Etagen mit Dachterrasse. Kaltmiete 1.300 €. Bahnhofstraße 10, 98553 Schleusingen. Ab sofort verfügbar." />
@@ -58,12 +187,15 @@
</script> </script>
<link rel="stylesheet" href="fonts/fonts.css" /> <link rel="stylesheet" href="fonts/fonts.css" />
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<link rel="stylesheet" href="css/haus-schleusingen.css" /> <link rel="stylesheet" href="css/haus-schleusingen.css" />
</head> </head>
<body> <body>
<nav id="navbar"> <a href="#main-content" class="skip-link">Zum Inhalt springen</a>
<nav id="navbar" role="navigation" aria-label="Hauptnavigation">
<div class="nav-logo">Bahnhofstraße 10</div> <div class="nav-logo">Bahnhofstraße 10</div>
<button class="nav-hamburger" aria-label="Navigation öffnen" aria-expanded="false">
<span></span>
</button>
<ul class="nav-links"> <ul class="nav-links">
<li><a href="#galerie">Galerie</a></li> <li><a href="#galerie">Galerie</a></li>
<li><a href="#grundriss">Grundriss</a></li> <li><a href="#grundriss">Grundriss</a></li>
@@ -77,6 +209,7 @@
Jetzt anfragen Jetzt anfragen
</button> </button>
</nav> </nav>
<div class="nav-mobile-overlay" aria-hidden="true"></div>
<section class="hero" id="hero"> <section class="hero" id="hero">
<div <div
@@ -107,6 +240,7 @@
</div> </div>
</section> </section>
<main id="main-content">
<div class="facts-strip"> <div class="facts-strip">
<div class="fact"> <div class="fact">
<div class="fact-val">227</div> <div class="fact-val">227</div>
@@ -160,7 +294,7 @@
</div> </div>
</section> </section>
<section id="galerie" class="gallery-section"> <section id="galerie" class="gallery-section" aria-label="Fotogalerie">
<div class="gallery-header"> <div class="gallery-header">
<div> <div>
<div class="section-eyebrow">Fotogalerie</div> <div class="section-eyebrow">Fotogalerie</div>
@@ -170,54 +304,54 @@
<div class="masonry-grid"> <div class="masonry-grid">
<div class="grid-sizer"></div> <div class="grid-sizer"></div>
<div class="grid-item" data-img="bilder/Außenansicht-2.png"> <div class="grid-item" data-img="bilder/Außenansicht-2.png" role="button" tabindex="0" aria-label="Außenansicht Großansicht öffnen">
<img src="bilder/Außenansicht-2-small.png" alt="Außenansicht" /> <img src="bilder/Außenansicht-2-small.png" alt="Außenansicht des Einfamilienhauses" />
<span class="grid-item-label">Außenansicht</span> <span class="grid-item-label">Außenansicht</span>
</div> </div>
<div class="grid-item" data-img="bilder/wohnzimmer2.png"> <div class="grid-item" data-img="bilder/wohnzimmer2.png" role="button" tabindex="0" aria-label="Wohnzimmer Großansicht öffnen">
<img src="bilder/wohnzimmer2-small.png" alt="Wohnzimmer" /> <img src="bilder/wohnzimmer2-small.png" alt="Wohnzimmer mit 42,6 m² Wohnfläche" />
<span class="grid-item-label">Wohnzimmer · 42,6 </span> <span class="grid-item-label">Wohnzimmer · 42,6 </span>
</div> </div>
<div class="grid-item" data-img="bilder/Küche 1.jpg"> <div class="grid-item" data-img="bilder/Küche 1.jpg" role="button" tabindex="0" aria-label="Küche Großansicht öffnen">
<img src="bilder/Küche 1.jpg" alt="Küche" /> <img src="bilder/Küche 1.jpg" alt="Küche mit 18,4 m²" />
<span class="grid-item-label">Küche · 18,4 </span> <span class="grid-item-label">Küche · 18,4 </span>
</div> </div>
<div class="grid-item" data-img="bilder/schlafzimmer.png"> <div class="grid-item" data-img="bilder/schlafzimmer.png" role="button" tabindex="0" aria-label="Schlafzimmer Großansicht öffnen">
<img src="bilder/schlafzimmer-small.png" alt="Schlafzimmer" /> <img src="bilder/schlafzimmer-small.png" alt="Schlafzimmer mit 18 m²" />
<span class="grid-item-label">Schlafzimmer · 18 </span> <span class="grid-item-label">Schlafzimmer · 18 </span>
</div> </div>
<div class="grid-item" data-img="bilder/Bad.jpg"> <div class="grid-item" data-img="bilder/Bad.jpg" role="button" tabindex="0" aria-label="Badezimmer Großansicht öffnen">
<img src="bilder/Bad.jpg" alt="Badezimmer" /> <img src="bilder/Bad.jpg" alt="Badezimmer mit 9,8 m²" />
<span class="grid-item-label">Badezimmer · 9,8 </span> <span class="grid-item-label">Badezimmer · 9,8 </span>
</div> </div>
<div class="grid-item" data-img="bilder/Kinderzimmer.png"> <div class="grid-item" data-img="bilder/Kinderzimmer.png" role="button" tabindex="0" aria-label="Kinderzimmer 1 Großansicht öffnen">
<img src="bilder/Kinderzimmer-small.png" alt="Kinderzimmer 1" /> <img src="bilder/Kinderzimmer-small.png" alt="Kinderzimmer 1 mit 21,7 m²" />
<span class="grid-item-label">Kinderzimmer 1 · 21,7 </span> <span class="grid-item-label">Kinderzimmer 1 · 21,7 </span>
</div> </div>
<div class="grid-item" data-img="bilder/Kinderzimmer 2.jpg"> <div class="grid-item" data-img="bilder/Kinderzimmer 2.jpg" role="button" tabindex="0" aria-label="Kinderzimmer 2 Großansicht öffnen">
<img src="bilder/Kinderzimmer 2-small.png" alt="Kinderzimmer 2" /> <img src="bilder/Kinderzimmer 2-small.png" alt="Kinderzimmer 2 mit 15,7 m²" />
<span class="grid-item-label">Kinderzimmer 2 · 15,7 </span> <span class="grid-item-label">Kinderzimmer 2 · 15,7 </span>
</div> </div>
<div class="grid-item" data-img="bilder/kinderzimmer 2 2.jpeg"> <div class="grid-item" data-img="bilder/kinderzimmer 2 2.jpeg" role="button" tabindex="0" aria-label="Kinderzimmer Detail Großansicht öffnen">
<img src="bilder/kinderzimmer 2 2-small.png" alt="Kinderzimmer Detail" /> <img src="bilder/kinderzimmer 2 2-small.png" alt="Detailansicht Kinderzimmer" />
<span class="grid-item-label">Kinderzimmer Detail</span> <span class="grid-item-label">Kinderzimmer Detail</span>
</div> </div>
<div class="grid-item" data-img="bilder/Kinderzimmer 3.jpg"> <div class="grid-item" data-img="bilder/Kinderzimmer 3.jpg" role="button" tabindex="0" aria-label="Gästezimmer Großansicht öffnen">
<img src="bilder/Kinderzimmer 3-small.png" alt="Kinderzimmer 3" /> <img src="bilder/Kinderzimmer 3-small.png" alt="Gästezimmer mit 11,5 m²" />
<span class="grid-item-label">Gästezimmer · 11,5 </span> <span class="grid-item-label">Gästezimmer · 11,5 </span>
</div> </div>
<div class="grid-item" data-img="bilder/Bad-2.jpg"> <div class="grid-item" data-img="bilder/Bad-2.jpg" role="button" tabindex="0" aria-label="Zweites Bad Großansicht öffnen">
<img src="bilder/Bad-2-small.jpg" alt="Wohnbereich Detail 1" /> <img src="bilder/Bad-2-small.jpg" alt="Zweites Badezimmer im Haus" />
<span class="grid-item-label">Wohnbereich</span> <span class="grid-item-label">Wohnbereich</span>
</div> </div>
<div class="grid-item" data-img="bilder/bad3.jpg"> <div class="grid-item" data-img="bilder/bad3.jpg" role="button" tabindex="0" aria-label="Drittes Bad Großansicht öffnen">
<img src="bilder/Bad-3-small.jpg" alt="Wohnbereich Detail 2" /> <img src="bilder/Bad-3-small.jpg" alt="Drittes Badezimmer im Haus" />
<span class="grid-item-label">Wohnbereich Detail</span> <span class="grid-item-label">Wohnbereich Detail</span>
</div> </div>
<div class="grid-item" data-img="bilder/WhatsApp Image 2026-03-30 at 07.50.42 (2).jpeg"> <div class="grid-item" data-img="bilder/WhatsApp Image 2026-03-30 at 07.50.42 (2).jpeg" role="button" tabindex="0" aria-label="Hausansicht Großansicht öffnen">
<img <img
src="bilder/WhatsApp Image 2026-03-30 at 07.50.42 (2).jpeg" src="bilder/WhatsApp Image 2026-03-30 at 07.50.42 (2).jpeg"
alt="Wohnbereich Detail 3" alt="Weitere Außenansicht des Einfamilienhauses"
/> />
<span class="grid-item-label">Hausansicht</span> <span class="grid-item-label">Hausansicht</span>
</div> </div>
@@ -229,14 +363,14 @@
<h2>Großzügig auf allen Etagen</h2> <h2>Großzügig auf allen Etagen</h2>
<div class="floor-accordion"> <div class="floor-accordion">
<div class="floor-item"> <div class="floor-item">
<div class="floor-header"> <div class="floor-header" role="button" tabindex="0" aria-expanded="false" aria-controls="floor-body-0" id="floor-title-0">
<span class="floor-title">Erdgeschoss</span> <span class="floor-title">Erdgeschoss</span>
<div class="floor-size"> <div class="floor-size">
<span>99,5 </span> <span>99,5 </span>
<div class="floor-icon">+</div> <div class="floor-icon">+</div>
</div> </div>
</div> </div>
<div class="floor-body"> <div class="floor-body" id="floor-body-0" role="region" aria-labelledby="floor-title-0">
<div class="floor-rooms-grid"> <div class="floor-rooms-grid">
<div class="room-chip"> <div class="room-chip">
Flur Flur
@@ -278,14 +412,14 @@
</div> </div>
</div> </div>
<div class="floor-item"> <div class="floor-item">
<div class="floor-header"> <div class="floor-header" role="button" tabindex="0" aria-expanded="false" aria-controls="floor-body-1" id="floor-title-1">
<span class="floor-title">1. Obergeschoss</span> <span class="floor-title">1. Obergeschoss</span>
<div class="floor-size"> <div class="floor-size">
<span>120,4 </span> <span>120,4 </span>
<div class="floor-icon">+</div> <div class="floor-icon">+</div>
</div> </div>
</div> </div>
<div class="floor-body"> <div class="floor-body" id="floor-body-1" role="region" aria-labelledby="floor-title-1">
<div class="floor-rooms-grid"> <div class="floor-rooms-grid">
<div class="room-chip"> <div class="room-chip">
Flur Flur
@@ -327,14 +461,14 @@
</div> </div>
</div> </div>
<div class="floor-item"> <div class="floor-item">
<div class="floor-header"> <div class="floor-header" role="button" tabindex="0" aria-expanded="false" aria-controls="floor-body-2" id="floor-title-2">
<span class="floor-title">2. Obergeschoss</span> <span class="floor-title">2. Obergeschoss</span>
<div class="floor-size"> <div class="floor-size">
<span>68 </span> <span>68 </span>
<div class="floor-icon">+</div> <div class="floor-icon">+</div>
</div> </div>
</div> </div>
<div class="floor-body"> <div class="floor-body" id="floor-body-2" role="region" aria-labelledby="floor-title-2">
<div class="floor-rooms-grid"> <div class="floor-rooms-grid">
<div class="room-chip"> <div class="room-chip">
Flur Flur
@@ -376,14 +510,14 @@
</div> </div>
</div> </div>
<div class="floor-item"> <div class="floor-item">
<div class="floor-header"> <div class="floor-header" role="button" tabindex="0" aria-expanded="false" aria-controls="floor-body-3" id="floor-title-3">
<span class="floor-title">Dachboden</span> <span class="floor-title">Dachboden</span>
<div class="floor-size"> <div class="floor-size">
<span>94 Nutzfläche</span> <span>94 Nutzfläche</span>
<div class="floor-icon">+</div> <div class="floor-icon">+</div>
</div> </div>
</div> </div>
<div class="floor-body"> <div class="floor-body" id="floor-body-3" role="region" aria-labelledby="floor-title-3">
<div class="floor-rooms-grid"> <div class="floor-rooms-grid">
<div class="room-chip"> <div class="room-chip">
Dachboden unten (ungeheizt) Dachboden unten (ungeheizt)
@@ -415,7 +549,7 @@
</div> </div>
</section> </section>
<section class="pricing-section" id="miete"> <section class="pricing-section" id="miete" aria-label="Mietkonditionen">
<div class="pricing-inner"> <div class="pricing-inner">
<div class="section-eyebrow">Mietkonditionen</div> <div class="section-eyebrow">Mietkonditionen</div>
<h2>Transparente Preisgestaltung</h2> <h2>Transparente Preisgestaltung</h2>
@@ -513,7 +647,7 @@
</div> </div>
</section> </section>
<section class="contact-section" id="kontakt"> <section class="contact-section" id="kontakt" aria-label="Kontaktformular">
<div class="contact-inner"> <div class="contact-inner">
<div class="section-eyebrow">Kontakt</div> <div class="section-eyebrow">Kontakt</div>
<h2> <h2>
@@ -527,15 +661,31 @@
paar Terminvorschläge an. paar Terminvorschläge an.
</p> </p>
<div class="contact-form"> <div class="contact-form">
<form id="contactForm"> <?php if ($formSuccess): ?>
<div id="form-result" class="form-success" style="display: block">
<p>Vielen Dank für Ihre Anfrage!</p>
<br />
<small>Wir haben Ihre Nachricht erhalten und melden uns innerhalb von 24 Stunden bei Ihnen.</small>
</div>
<?php else: ?>
<?php if (!empty($formErrors)): ?>
<div id="form-result" class="form-errors">
<ul>
<?php foreach ($formErrors as $error): ?>
<li><?= escapeContactValue($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form id="contactForm" method="post">
<div class="form-row"> <div class="form-row">
<div class="form-field"> <div class="form-field">
<label for="fname">Vorname</label> <label for="fname">Vorname</label>
<input type="text" id="fname" name="fname" placeholder="Max" required /> <input type="text" id="fname" name="fname" placeholder="Max" required value="<?= escapeContactValue($formData['fname']) ?>" />
</div> </div>
<div class="form-field"> <div class="form-field">
<label for="lname">Nachname</label> <label for="lname">Nachname</label>
<input type="text" id="lname" name="lname" placeholder="Mustermann" required /> <input type="text" id="lname" name="lname" placeholder="Mustermann" required value="<?= escapeContactValue($formData['lname']) ?>" />
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
@@ -547,20 +697,25 @@
name="email" name="email"
placeholder="max@beispiel.de" placeholder="max@beispiel.de"
required required
value="<?= escapeContactValue($formData['email']) ?>"
/> />
</div> </div>
<div class="form-field"> <div class="form-field">
<label for="phone">Telefon</label> <label for="phone">Telefon</label>
<input type="tel" id="phone" name="phone" placeholder="+49 ..." /> <input type="tel" id="phone" name="phone" placeholder="+49 ..." value="<?= escapeContactValue($formData['phone']) ?>" />
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-field full"> <div class="form-field full">
<label for="interest">Anliegen</label> <label for="interest">Anliegen</label>
<select id="interest" name="interest"> <select id="interest" name="interest">
<option>Besichtigung anfragen</option> <?php
<option>Allgemeine Informationen</option> $interestOptions = ['Besichtigung anfragen', 'Allgemeine Informationen', 'Mietbewerbung einreichen'];
<option>Mietbewerbung einreichen</option> foreach ($interestOptions as $opt):
$selected = ($formData['interest'] === $opt) ? ' selected' : '';
?>
<option<?= $selected ?>><?= escapeContactValue($opt) ?></option>
<?php endforeach; ?>
</select> </select>
</div> </div>
</div> </div>
@@ -572,16 +727,20 @@
name="message" name="message"
rows="4" rows="4"
placeholder="Ihre Nachricht ..." placeholder="Ihre Nachricht ..."
></textarea> required
><?= escapeContactValue($formData['message']) ?></textarea>
</div> </div>
</div> </div>
<!-- Honeypot: hidden field for spam bots -->
<div class="hp-field" aria-hidden="true">
<label for="website">Website</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off" />
</div>
<!-- Form load timestamp for minimum-submit-time check -->
<input type="hidden" name="form_time" value="<?= time() ?>" />
<button type="submit" class="btn-submit">Anfrage absenden</button> <button type="submit" class="btn-submit">Anfrage absenden</button>
</form> </form>
<div class="form-success" id="formSuccess"> <?php endif; ?>
<p>Vielen Dank für Ihre Anfrage!</p>
<br />
<small>Ihr E-Mail-Programm wurde geöffnet. Bitte senden Sie die E-Mail ab, damit Ihre Anfrage bei uns eingeht.</small>
</div>
</div> </div>
<div class="contact-details"> <div class="contact-details">
<p>Oder schreiben Sie uns direkt: <a href="mailto:mki@kies-media.de">mki@kies-media.de</a></p> <p>Oder schreiben Sie uns direkt: <a href="mailto:mki@kies-media.de">mki@kies-media.de</a></p>
@@ -589,7 +748,8 @@
</div> </div>
</section> </section>
<footer> </main>
<footer role="contentinfo">
<div class="footer-logo">Bahnhofstraße 10 · Schleusingen</div> <div class="footer-logo">Bahnhofstraße 10 · Schleusingen</div>
<div class="footer-links"> <div class="footer-links">
<a href="impressum.html">Impressum</a> <a href="impressum.html">Impressum</a>
@@ -597,9 +757,9 @@
</div> </div>
</footer> </footer>
<div class="lightbox" id="lightbox"> <div class="lightbox" id="lightbox" role="dialog" aria-modal="true" aria-label="Bildansicht">
<button class="lightbox-close" id="lightboxClose">&times;</button> <button class="lightbox-close" id="lightboxClose" aria-label="Bildansicht schließen">&times;</button>
<img src="" id="lightboxImg" alt="Vollbild" /> <img src="" id="lightboxImg" alt="" />
</div> </div>
<script src="js/haus-schleusingen.js"></script> <script src="js/haus-schleusingen.js"></script>

View File

@@ -1,86 +1,208 @@
$(function () { document.addEventListener("DOMContentLoaded", function () {
// Navbar scroll // Navbar scroll
$(window).on("scroll", function () { var navbar = document.getElementById("navbar");
if ($(this).scrollTop() > 60) $("#navbar").addClass("scrolled"); window.addEventListener("scroll", function () {
else $("#navbar").removeClass("scrolled"); if (window.scrollY > 60) navbar.classList.add("scrolled");
else navbar.classList.remove("scrolled");
}); });
// Hero animation on load // Hero animation on load
setTimeout(function () { setTimeout(function () {
$("#heroContent").addClass("visible"); document.getElementById("heroContent").classList.add("visible");
$("#heroBg").addClass("loaded"); document.getElementById("heroBg").classList.add("loaded");
}, 200); }, 200);
// Scroll animations // Scroll animations via IntersectionObserver
function checkVisible() { var animElements = document.querySelectorAll(".fact, [data-animate]");
$(".fact, [data-animate]").each(function () { animElements.forEach(function (el) {
var el = $(this); el.style.opacity = "0";
var top = el.offset().top; el.style.transform = "translateY(30px)";
var windowBottom = $(window).scrollTop() + $(window).height(); el.style.transition = "opacity 0.8s ease, transform 0.8s ease";
if (windowBottom > top + 60) { });
el.addClass("visible");
el.css({ opacity: 1, transform: "translateY(0)" }); if ("IntersectionObserver" in window) {
var observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
entry.target.style.opacity = "1";
entry.target.style.transform = "translateY(0)";
observer.unobserve(entry.target);
} }
}); });
} },
$("[data-animate]").css({ { rootMargin: "0px 0px -60px 0px" }
opacity: 0, );
transform: "translateY(30px)", animElements.forEach(function (el) {
transition: "opacity 0.8s ease, transform 0.8s ease", observer.observe(el);
}); });
$(window).on("scroll", checkVisible); } else {
checkVisible(); // Fallback: show all immediately
animElements.forEach(function (el) {
el.classList.add("visible");
el.style.opacity = "1";
el.style.transform = "translateY(0)";
});
}
// Floor accordion // Floor accordion
$(".floor-header").on("click", function () { // Floor accordion (vanilla JS + a11y)
var item = $(this).closest(".floor-item"); document.querySelectorAll(".floor-header").forEach(function (header) {
var isOpen = item.hasClass("open"); header.addEventListener("click", function () {
$(".floor-item").removeClass("open"); var item = this.closest(".floor-item");
$(".floor-body").slideUp(300); var isOpen = item.classList.contains("open");
var allItems = document.querySelectorAll(".floor-item");
// Close all
allItems.forEach(function (fi) {
fi.classList.remove("open");
var hdr = fi.querySelector(".floor-header");
if (hdr) hdr.setAttribute("aria-expanded", "false");
var body = fi.querySelector(".floor-body");
if (body) body.style.display = "none";
});
// Open clicked if it was closed
if (!isOpen) { if (!isOpen) {
item.addClass("open"); item.classList.add("open");
item.find(".floor-body").slideDown(300); this.setAttribute("aria-expanded", "true");
var body = item.querySelector(".floor-body");
if (body) body.style.display = "block";
}
});
});
// Accordion keyboard handler (Enter/Space)
document.querySelectorAll(".floor-header").forEach(function (header) {
header.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
this.click();
}
});
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
$(this).trigger("click");
} }
}); });
// Lightbox gallery grid items // Lightbox track last focused element for focus return
$(document).on("click", ".grid-item", function () { var lightboxTrigger = null;
var src = $(this).data("img") || $(this).find("img").attr("src");
$("#lightboxImg").attr("src", src); function openLightbox(src) {
lightboxTrigger = document.activeElement;
$("#lightboxImg").attr("src", src).attr("alt", "");
$("#lightbox").addClass("open"); $("#lightbox").addClass("open");
$("body").css("overflow", "hidden"); $("body").css("overflow", "hidden");
// Set focus to close button
setTimeout(function () {
$("#lightboxClose").focus();
}, 50);
}
function closeLightbox() {
$("#lightbox").removeClass("open");
$("body").css("overflow", "");
// Return focus to trigger
if (lightboxTrigger) {
$(lightboxTrigger).focus();
lightboxTrigger = null;
}
}
// Lightbox gallery grid items
// Lightbox gallery grid items
document.querySelectorAll(".grid-item").forEach(function (item) {
item.addEventListener("click", function () {
var src = this.dataset.img || this.querySelector("img").getAttribute("src");
openLightbox(src);
});
});
// Gallery keyboard handler (Enter/Space)
document.querySelectorAll(".grid-item").forEach(function (item) {
item.setAttribute("tabindex", "0");
item.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
this.click();
}
});
}); });
// Lightbox floor plan images in Raumaufteilung // Lightbox floor plan images in Raumaufteilung
$(document).on("click", ".floor-plan img[data-img]", function () { document.querySelectorAll(".floor-plan img[data-img]").forEach(function (img) {
var src = $(this).data("img"); img.addEventListener("click", function () {
$("#lightboxImg").attr("src", src); openLightbox(this.dataset.img);
$("#lightbox").addClass("open");
$("body").css("overflow", "hidden");
}); });
$("#lightboxClose, #lightbox").on("click", function (e) { });
if (e.target === this) {
$("#lightbox").removeClass("open"); function openLightbox(src) {
$("body").css("overflow", ""); lightboxTrigger = document.activeElement;
document.getElementById("lightboxImg").setAttribute("src", src);
document.getElementById("lightbox").classList.add("open");
document.body.style.overflow = "hidden";
// Focus close button
setTimeout(function () {
document.getElementById("lightboxClose").focus();
}, 100);
} }
function closeLightbox() {
document.getElementById("lightbox").classList.remove("open");
document.body.style.overflow = "";
// Return focus to trigger
if (lightboxTrigger) {
lightboxTrigger.focus();
lightboxTrigger = null;
}
}
document.getElementById("lightboxClose").addEventListener("click", closeLightbox);
document.getElementById("lightbox").addEventListener("click", function (e) {
if (e.target === this) closeLightbox();
}); });
$(document).on("keydown", function (e) { document.addEventListener("keydown", function (e) {
if (e.key === "Escape") { if (e.key === "Escape") closeLightbox();
$("#lightbox").removeClass("open"); });
$("body").css("overflow", "");
// Focus trap for lightbox
document.getElementById("lightbox").addEventListener("keydown", function (e) {
if (e.key !== "Tab") return;
var focusable = this.querySelectorAll("button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])");
focusable = Array.from(focusable).filter(function (el) { return el.offsetParent !== null; });
if (focusable.length === 0) return;
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
} }
}); });
// Form submit is handled server-side by PHP no JS intervention needed.
// Form submit opens email client with pre-filled mailto: link // Form submit opens email client with pre-filled mailto: link
$("#contactForm").on("submit", function (e) { document.getElementById("contactForm").addEventListener("submit", function (e) {
e.preventDefault(); e.preventDefault();
var fname = $("#fname").val().trim(); var fname = document.getElementById("fname").value.trim();
var lname = $("#lname").val().trim(); var lname = document.getElementById("lname").value.trim();
var email = $("#email").val().trim(); var email = document.getElementById("email").value.trim();
var phone = $("#phone").val().trim(); var phone = document.getElementById("phone").value.trim();
var interest = $("#interest").val(); var interest = document.getElementById("interest").value;
var message = $("#message").val().trim(); var message = document.getElementById("message").value.trim();
var subject = "Kontaktanfrage: " + interest; var subject = "Kontaktanfrage: " + interest;
var body = "Von: " + fname + " " + lname + "\n"; var body = "Von: " + fname + " " + lname + "\n";
@@ -97,7 +219,58 @@ $(function () {
window.location.href = mailto; window.location.href = mailto;
// Show success message // Show success message
$("#contactForm").hide(); this.style.display = "none";
$("#formSuccess").fadeIn(400); var success = document.getElementById("formSuccess");
success.style.display = "block";
success.style.opacity = "0";
success.style.transition = "opacity 0.4s ease";
requestAnimationFrame(function () {
success.style.opacity = "1";
});
}); });
}); });
// Mobile hamburger menu (vanilla JS)
(function () {
var hamburger = document.querySelector(".nav-hamburger");
var nav = document.getElementById("navbar");
var overlay = document.querySelector(".nav-mobile-overlay");
var links = nav ? nav.querySelectorAll(".nav-links a") : [];
function toggleMenu() {
var isOpen = hamburger.classList.toggle("active");
nav.classList.toggle("mobile-open", isOpen);
if (overlay) overlay.classList.toggle("active", isOpen);
hamburger.setAttribute("aria-expanded", isOpen);
document.body.style.overflow = isOpen ? "hidden" : "";
}
function closeMenu() {
hamburger.classList.remove("active");
nav.classList.remove("mobile-open");
if (overlay) overlay.classList.remove("active");
hamburger.setAttribute("aria-expanded", "false");
document.body.style.overflow = "";
}
if (hamburger) {
hamburger.addEventListener("click", toggleMenu);
}
if (overlay) {
overlay.addEventListener("click", closeMenu);
}
links.forEach(function (link) {
link.addEventListener("click", closeMenu);
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") closeMenu();
});
// Close on resize to desktop
window.addEventListener("resize", function () {
if (window.innerWidth > 900) closeMenu();
});
})();

File diff suppressed because one or more lines are too long