5 Commits

Author SHA1 Message Date
Hermes
41ade292c4 fix(map): correct OSM marker to actual address (was 556m north-east)
All checks were successful
Lint / PHP Syntax Check (push) Successful in 59s
PHPUnit / PHP Unit Tests (push) Successful in 1m7s
Lint / HTML Lint (htmlhint) (push) Successful in 1m35s
Lint / CSS Lint (stylelint) (push) Successful in 1m41s
2026-06-14 17:06:03 +00:00
Hermes
ac8b3f4d79 fix(css): constrain Impressum + Datenschutz to 1200px container with minimal typografie
All checks were successful
Lint / PHP Syntax Check (push) Successful in 57s
PHPUnit / PHP Unit Tests (push) Successful in 1m7s
Lint / HTML Lint (htmlhint) (push) Successful in 1m36s
Lint / CSS Lint (stylelint) (push) Successful in 1m40s
2026-06-14 17:03:12 +00:00
Hermes
a25eac3b04 fix(css): constrain gallery heading, pricing + contact sections to main container width
All checks were successful
Lint / PHP Syntax Check (push) Successful in 29s
PHPUnit / PHP Unit Tests (push) Successful in 37s
Lint / CSS Lint (stylelint) (push) Successful in 1m13s
Lint / HTML Lint (htmlhint) (push) Successful in 1m7s
2026-06-14 16:36:03 +00:00
Hermes
d34fc00078 fix(issue-80): full-width floor plan images + styled room list + restored + icon
All checks were successful
Lint / PHP Syntax Check (push) Successful in 58s
PHPUnit / PHP Unit Tests (push) Successful in 1m8s
Lint / PHP Syntax Check (pull_request) Successful in 29s
Lint / HTML Lint (htmlhint) (push) Successful in 1m39s
Lint / CSS Lint (stylelint) (push) Successful in 1m43s
PHPUnit / PHP Unit Tests (pull_request) Successful in 36s
Lint / CSS Lint (stylelint) (pull_request) Successful in 1m11s
Lint / HTML Lint (htmlhint) (pull_request) Successful in 1m5s
2026-06-14 15:38:35 +00:00
Hermes
db83e09a3a fix(issue-80): show all 13 house images + 3D floor plans
All checks were successful
Lint / PHP Syntax Check (push) Successful in 57s
PHPUnit / PHP Unit Tests (push) Successful in 1m7s
Lint / HTML Lint (htmlhint) (push) Successful in 1m38s
Lint / CSS Lint (stylelint) (push) Successful in 1m42s
Lint / PHP Syntax Check (pull_request) Successful in 58s
PHPUnit / PHP Unit Tests (pull_request) Successful in 1m3s
Lint / HTML Lint (htmlhint) (pull_request) Successful in 1m36s
Lint / CSS Lint (stylelint) (pull_request) Successful in 1m41s
2026-06-05 22:28:04 +00:00
8 changed files with 346 additions and 169 deletions

View File

@@ -36,19 +36,19 @@ jobs:
mkdir -p ~/.ssh mkdir -p ~/.ssh
echo "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519 echo "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H -p 9716 46.225.136.66 >> ~/.ssh/known_hosts 2>/dev/null ssh-keyscan -H 188.245.242.194 >> ~/.ssh/known_hosts 2>/dev/null
echo "✅ SSH key configured" echo "✅ SSH key configured"
- name: Verify SSH connectivity - name: Verify SSH connectivity
if: env.skip_deploy != '1' if: env.skip_deploy != '1'
run: | run: |
ssh -o StrictHostKeyChecking=yes -i ~/.ssh/id_ed25519 -p 9716 \ ssh -o StrictHostKeyChecking=yes -i ~/.ssh/id_ed25519 \
root@46.225.136.66 "echo 'SSH-OK as:' \$(whoami) 'on' \$(hostname)" haustest@188.245.242.194 "echo 'SSH-OK as:' \$(whoami) 'on' \$(hostname)"
- name: Backup current test deployment - name: Backup current test deployment
if: env.skip_deploy != '1' if: env.skip_deploy != '1'
run: | run: |
ssh -i ~/.ssh/id_ed25519 -p 9716 root@46.225.136.66 \ ssh -i ~/.ssh/id_ed25519 haustest@188.245.242.194 \
"cd /home/haustest/htdocs && \ "cd /home/haustest/htdocs && \
tar czf /home/haustest/backup-pre-deploy-\$(date +%Y%m%d-%H%M%S).tar.gz \ tar czf /home/haustest/backup-pre-deploy-\$(date +%Y%m%d-%H%M%S).tar.gz \
haus.test.kies-media.de && \ haus.test.kies-media.de && \
@@ -83,16 +83,8 @@ jobs:
--exclude='.stylelintrc.json' \ --exclude='.stylelintrc.json' \
--exclude='.editorconfig' \ --exclude='.editorconfig' \
--exclude='.well-known' \ --exclude='.well-known' \
-e "ssh -i ~/.ssh/id_ed25519 -p 9716" \ -e "ssh -i ~/.ssh/id_ed25519" \
./ root@46.225.136.66:/home/haustest/htdocs/haus.test.kies-media.de/ ./ haustest@188.245.242.194:/home/haustest/htdocs/haus.test.kies-media.de/
- name: Fix ownership and install dependencies
if: env.skip_deploy != '1'
run: |
ssh -i ~/.ssh/id_ed25519 -p 9716 root@46.225.136.66 \
"cd /home/haustest/htdocs/haus.test.kies-media.de && \
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --no-interaction --no-progress && \
chown -R haustest:haustest /home/haustest/htdocs/haus.test.kies-media.de"
- name: Smoke test - name: Smoke test
if: env.skip_deploy != '1' if: env.skip_deploy != '1'
@@ -137,6 +129,6 @@ jobs:
echo "- **Target:** https://haus.test.kies-media.de" >> $GITHUB_STEP_SUMMARY echo "- **Target:** https://haus.test.kies-media.de" >> $GITHUB_STEP_SUMMARY
echo "- **Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY echo "- **Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY echo "- **Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Server:** root@46.225.136.66:9716 (cp3)" >> $GITHUB_STEP_SUMMARY echo "- **Server:** haustest@188.245.242.194" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "**Review URL:** https://haus.test.kies-media.de" >> $GITHUB_STEP_SUMMARY echo "**Review URL:** https://haus.test.kies-media.de" >> $GITHUB_STEP_SUMMARY

View File

@@ -69,6 +69,10 @@ return [
'gallery.kid1' => 'Kinderzimmer 1 · 21,7 m²', 'gallery.kid1' => 'Kinderzimmer 1 · 21,7 m²',
'gallery.kid2' => 'Kinderzimmer 2 · 15,7 m²', 'gallery.kid2' => 'Kinderzimmer 2 · 15,7 m²',
'gallery.kid_detail' => 'Kinderzimmer Detail', 'gallery.kid_detail' => 'Kinderzimmer Detail',
'gallery.kid_extra' => 'Kinderzimmer · Spielbereich',
'gallery.bath2' => 'Badezimmer · 6,4 m²',
'gallery.bath3' => 'Badezimmer · 5,8 m²',
'gallery.bath4' => 'Badezimmer · Wellness',
'gallery.guest' => 'Gästezimmer · 11,5 m²', 'gallery.guest' => 'Gästezimmer · 11,5 m²',
'gallery.area1' => 'Wohnbereich', 'gallery.area1' => 'Wohnbereich',
'gallery.area2' => 'Wohnbereich Detail', 'gallery.area2' => 'Wohnbereich Detail',
@@ -80,9 +84,11 @@ return [
'gallery.alt.kid1' => 'Kinderzimmer 1 mit 21,7 m²', 'gallery.alt.kid1' => 'Kinderzimmer 1 mit 21,7 m²',
'gallery.alt.kid2' => 'Kinderzimmer 2 mit 15,7 m²', 'gallery.alt.kid2' => 'Kinderzimmer 2 mit 15,7 m²',
'gallery.alt.kid_detail' => 'Detailansicht Kinderzimmer', 'gallery.alt.kid_detail' => 'Detailansicht Kinderzimmer',
'gallery.alt.kid_extra' => 'Spielbereich im Kinderzimmer',
'gallery.alt.guest' => 'Gästezimmer mit 11,5 m²', 'gallery.alt.guest' => 'Gästezimmer mit 11,5 m²',
'gallery.alt.bath2' => 'Zweites Badezimmer im Haus', 'gallery.alt.bath2' => 'Zweites Badezimmer im Haus',
'gallery.alt.bath3' => 'Drittes Badezimmer im Haus', 'gallery.alt.bath3' => 'Drittes Badezimmer im Haus',
'gallery.alt.bath4' => 'Viertes Badezimmer im Haus',
'gallery.alt.exterior' => 'Außenansicht des Einfamilienhauses', 'gallery.alt.exterior' => 'Außenansicht des Einfamilienhauses',
// ─── Floor plans (Grundriss) ──────────────────────────────────────── // ─── Floor plans (Grundriss) ────────────────────────────────────────
@@ -120,6 +126,10 @@ return [
'floors.alt.og1' => 'Grundriss 1. Obergeschoss', 'floors.alt.og1' => 'Grundriss 1. Obergeschoss',
'floors.alt.og2' => 'Grundriss 2. Obergeschoss', 'floors.alt.og2' => 'Grundriss 2. Obergeschoss',
'floors.alt.attic' => 'Grundriss Dachboden', 'floors.alt.attic' => 'Grundriss Dachboden',
'floors.alt.eg_3d' => '3D-Ansicht Erdgeschoss',
'floors.alt.og1_3d' => '3D-Ansicht 1. Obergeschoss',
'floors.alt.og2_3d' => '3D-Ansicht 2. Obergeschoss',
'floors.alt.attic_2' => 'Alternative Ansicht Dachboden',
// ─── Rent (Miete) ──────────────────────────────────────────────────── // ─── Rent (Miete) ────────────────────────────────────────────────────
'rent.eyebrow' => 'Mietkonditionen', 'rent.eyebrow' => 'Mietkonditionen',

View File

@@ -59,6 +59,10 @@ return [
'gallery.kid1' => 'Child\'s room 1 · 21.7 m²', 'gallery.kid1' => 'Child\'s room 1 · 21.7 m²',
'gallery.kid2' => 'Child\'s room 2 · 15.7 m²', 'gallery.kid2' => 'Child\'s room 2 · 15.7 m²',
'gallery.kid_detail' => 'Child\'s room detail', 'gallery.kid_detail' => 'Child\'s room detail',
'gallery.kid_extra' => 'Child\'s room · play area',
'gallery.bath2' => 'Bathroom · 6.4 m²',
'gallery.bath3' => 'Bathroom · 5.8 m²',
'gallery.bath4' => 'Bathroom · wellness',
'gallery.guest' => 'Guest room · 11.5 m²', 'gallery.guest' => 'Guest room · 11.5 m²',
'gallery.area1' => 'Living area', 'gallery.area1' => 'Living area',
'gallery.area2' => 'Living area detail', 'gallery.area2' => 'Living area detail',
@@ -70,9 +74,11 @@ return [
'gallery.alt.kid1' => 'Child\'s room 1 with 21.7 m²', 'gallery.alt.kid1' => 'Child\'s room 1 with 21.7 m²',
'gallery.alt.kid2' => 'Child\'s room 2 with 15.7 m²', 'gallery.alt.kid2' => 'Child\'s room 2 with 15.7 m²',
'gallery.alt.kid_detail' => 'Child\'s room detail view', 'gallery.alt.kid_detail' => 'Child\'s room detail view',
'gallery.alt.kid_extra' => 'Play area in the child\'s room',
'gallery.alt.guest' => 'Guest room with 11.5 m²', 'gallery.alt.guest' => 'Guest room with 11.5 m²',
'gallery.alt.bath2' => 'Second bathroom in the house', 'gallery.alt.bath2' => 'Second bathroom in the house',
'gallery.alt.bath3' => 'Third bathroom in the house', 'gallery.alt.bath3' => 'Third bathroom in the house',
'gallery.alt.bath4' => 'Fourth bathroom in the house',
'gallery.alt.exterior' => 'Exterior view of the detached house', 'gallery.alt.exterior' => 'Exterior view of the detached house',
'floors.eyebrow' => 'Layout', 'floors.eyebrow' => 'Layout',
@@ -109,6 +115,10 @@ return [
'floors.alt.og1' => 'Floor plan 1st upper floor', 'floors.alt.og1' => 'Floor plan 1st upper floor',
'floors.alt.og2' => 'Floor plan 2nd upper floor', 'floors.alt.og2' => 'Floor plan 2nd upper floor',
'floors.alt.attic' => 'Floor plan attic', 'floors.alt.attic' => 'Floor plan attic',
'floors.alt.eg_3d' => '3D view ground floor',
'floors.alt.og1_3d' => '3D view 1st upper floor',
'floors.alt.og2_3d' => '3D view 2nd upper floor',
'floors.alt.attic_2' => 'Alternative view of the attic',
'rent.eyebrow' => 'Rental terms', 'rent.eyebrow' => 'Rental terms',
'rent.aria' => 'Rental terms', 'rent.aria' => 'Rental terms',

View File

@@ -59,6 +59,10 @@ return [
'gallery.kid1' => 'Детская 1 · 21,7 м²', 'gallery.kid1' => 'Детская 1 · 21,7 м²',
'gallery.kid2' => 'Детская 2 · 15,7 м²', 'gallery.kid2' => 'Детская 2 · 15,7 м²',
'gallery.kid_detail' => 'Деталь детской', 'gallery.kid_detail' => 'Деталь детской',
'gallery.kid_extra' => 'Детская · игровая зона',
'gallery.bath2' => 'Ванная · 6,4 м²',
'gallery.bath3' => 'Ванная · 5,8 м²',
'gallery.bath4' => 'Ванная · велнес',
'gallery.guest' => 'Гостевая комната · 11,5 м²', 'gallery.guest' => 'Гостевая комната · 11,5 м²',
'gallery.area1' => 'Жилая зона', 'gallery.area1' => 'Жилая зона',
'gallery.area2' => 'Деталь жилой зоны', 'gallery.area2' => 'Деталь жилой зоны',
@@ -70,9 +74,11 @@ return [
'gallery.alt.kid1' => 'Детская комната 1 — 21,7 м²', 'gallery.alt.kid1' => 'Детская комната 1 — 21,7 м²',
'gallery.alt.kid2' => 'Детская комната 2 — 15,7 м²', 'gallery.alt.kid2' => 'Детская комната 2 — 15,7 м²',
'gallery.alt.kid_detail' => 'Детальный вид детской комнаты', 'gallery.alt.kid_detail' => 'Детальный вид детской комнаты',
'gallery.alt.kid_extra' => 'Игровая зона в детской комнате',
'gallery.alt.guest' => 'Гостевая комната 11,5 м²', 'gallery.alt.guest' => 'Гостевая комната 11,5 м²',
'gallery.alt.bath2' => 'Вторая ванная комната в доме', 'gallery.alt.bath2' => 'Вторая ванная комната в доме',
'gallery.alt.bath3' => 'Третья ванная комната в доме', 'gallery.alt.bath3' => 'Третья ванная комната в доме',
'gallery.alt.bath4' => 'Четвертая ванная комната в доме',
'gallery.alt.exterior' => 'Внешний вид частного дома', 'gallery.alt.exterior' => 'Внешний вид частного дома',
'floors.eyebrow' => 'Планировка', 'floors.eyebrow' => 'Планировка',
@@ -109,6 +115,10 @@ return [
'floors.alt.og1' => 'План второго этажа', 'floors.alt.og1' => 'План второго этажа',
'floors.alt.og2' => 'План третьего этажа', 'floors.alt.og2' => 'План третьего этажа',
'floors.alt.attic' => 'План чердака', 'floors.alt.attic' => 'План чердака',
'floors.alt.eg_3d' => '3D-вид первого этажа',
'floors.alt.og1_3d' => '3D-вид второго этажа',
'floors.alt.og2_3d' => '3D-вид третьего этажа',
'floors.alt.attic_2' => 'Альтернативный вид чердака',
'rent.eyebrow' => 'Условия аренды', 'rent.eyebrow' => 'Условия аренды',
'rent.aria' => 'Условия аренды', 'rent.aria' => 'Условия аренды',

View File

@@ -59,6 +59,10 @@ return [
'gallery.kid1' => 'Дитяча кімната 1 · 21,7 м²', 'gallery.kid1' => 'Дитяча кімната 1 · 21,7 м²',
'gallery.kid2' => 'Дитяча кімната 2 · 15,7 м²', 'gallery.kid2' => 'Дитяча кімната 2 · 15,7 м²',
'gallery.kid_detail' => 'Деталь дитячої кімнати', 'gallery.kid_detail' => 'Деталь дитячої кімнати',
'gallery.kid_extra' => 'Дитяча кімната · ігрова зона',
'gallery.bath2' => 'Ванна кімната · 6,4 м²',
'gallery.bath3' => 'Ванна кімната · 5,8 м²',
'gallery.bath4' => 'Ванна кімната · велнес',
'gallery.guest' => 'Гостьова кімната · 11,5 м²', 'gallery.guest' => 'Гостьова кімната · 11,5 м²',
'gallery.area1' => 'Житлова зона', 'gallery.area1' => 'Житлова зона',
'gallery.area2' => 'Деталь житлової зони', 'gallery.area2' => 'Деталь житлової зони',
@@ -70,9 +74,11 @@ return [
'gallery.alt.kid1' => 'Дитяча кімната 1 — 21,7 м²', 'gallery.alt.kid1' => 'Дитяча кімната 1 — 21,7 м²',
'gallery.alt.kid2' => 'Дитяча кімната 2 — 15,7 м²', 'gallery.alt.kid2' => 'Дитяча кімната 2 — 15,7 м²',
'gallery.alt.kid_detail' => 'Детальний вигляд дитячої кімнати', 'gallery.alt.kid_detail' => 'Детальний вигляд дитячої кімнати',
'gallery.alt.kid_extra' => 'Ігрова зона в дитячій кімнаті',
'gallery.alt.guest' => 'Гостьова кімната 11,5 м²', 'gallery.alt.guest' => 'Гостьова кімната 11,5 м²',
'gallery.alt.bath2' => 'Друга ванна кімната в будинку', 'gallery.alt.bath2' => 'Друга ванна кімната в будинку',
'gallery.alt.bath3' => 'Третя ванна кімната в будинку', 'gallery.alt.bath3' => 'Третя ванна кімната в будинку',
'gallery.alt.bath4' => 'Четверта ванна кімната в будинку',
'gallery.alt.exterior' => 'Зовнішній вигляд приватного будинку', 'gallery.alt.exterior' => 'Зовнішній вигляд приватного будинку',
'floors.eyebrow' => 'Планування', 'floors.eyebrow' => 'Планування',
@@ -109,6 +115,10 @@ return [
'floors.alt.og1' => 'План другого поверху', 'floors.alt.og1' => 'План другого поверху',
'floors.alt.og2' => 'План третього поверху', 'floors.alt.og2' => 'План третього поверху',
'floors.alt.attic' => 'План горища', 'floors.alt.attic' => 'План горища',
'floors.alt.eg_3d' => '3D-вигляд першого поверху',
'floors.alt.og1_3d' => '3D-вигляд другого поверху',
'floors.alt.og2_3d' => '3D-вигляд третього поверху',
'floors.alt.attic_2' => 'Альтернативний вигляд горища',
'rent.eyebrow' => 'Умови оренди', 'rent.eyebrow' => 'Умови оренди',
'rent.aria' => 'Умови оренди', 'rent.aria' => 'Умови оренди',

View File

@@ -23,9 +23,13 @@ $gridItems = [
['img' => 'bilder/Küche 1.jpg', 'key' => 'gallery.kitchen', 'alt' => 'gallery.alt.kitchen', 'class' => ''], ['img' => 'bilder/Küche 1.jpg', 'key' => 'gallery.kitchen', 'alt' => 'gallery.alt.kitchen', 'class' => ''],
['img' => 'bilder/schlafzimmer.png', 'key' => 'gallery.bedroom', 'alt' => 'gallery.alt.bedroom', 'class' => ''], ['img' => 'bilder/schlafzimmer.png', 'key' => 'gallery.bedroom', 'alt' => 'gallery.alt.bedroom', 'class' => ''],
['img' => 'bilder/Bad.jpg', 'key' => 'gallery.bath', 'alt' => 'gallery.alt.bath', 'class' => ''], ['img' => 'bilder/Bad.jpg', 'key' => 'gallery.bath', 'alt' => 'gallery.alt.bath', 'class' => ''],
['img' => 'bilder/Bad-2.jpeg', 'key' => 'gallery.bath2', 'alt' => 'gallery.alt.bath2', 'class' => ''],
['img' => 'bilder/Bad-3.jpeg', 'key' => 'gallery.bath3', 'alt' => 'gallery.alt.bath3', 'class' => ''],
['img' => 'bilder/Bad-4.jpeg', 'key' => 'gallery.bath4', 'alt' => 'gallery.alt.bath4', 'class' => ''],
['img' => 'bilder/Kinderzimmer 2.jpg', 'key' => 'gallery.kid1', 'alt' => 'gallery.alt.kid1', 'class' => ''], ['img' => 'bilder/Kinderzimmer 2.jpg', 'key' => 'gallery.kid1', 'alt' => 'gallery.alt.kid1', 'class' => ''],
['img' => 'bilder/Kinderzimmer 3.jpg', 'key' => 'gallery.kid2', 'alt' => 'gallery.alt.kid2', 'class' => ''], ['img' => 'bilder/Kinderzimmer 3.jpg', 'key' => 'gallery.kid2', 'alt' => 'gallery.alt.kid2', 'class' => ''],
['img' => 'bilder/kinderzimmer 2 2.webp', 'key' => 'gallery.kid_detail', 'alt' => 'gallery.alt.kid_detail', 'class' => 'span-2 row-1'], ['img' => 'bilder/kinderzimmer 2 2.webp', 'key' => 'gallery.kid_detail', 'alt' => 'gallery.alt.kid_detail', 'class' => 'span-2 row-1'],
['img' => 'bilder/Kinderzimmer.png', 'key' => 'gallery.kid_extra', 'alt' => 'gallery.alt.kid_extra', 'class' => ''],
['img' => 'bilder/Außenansicht-2.png', 'key' => 'gallery.area3', 'alt' => 'gallery.alt.exterior', 'class' => 'span-2 row-1'], ['img' => 'bilder/Außenansicht-2.png', 'key' => 'gallery.area3', 'alt' => 'gallery.alt.exterior', 'class' => 'span-2 row-1'],
]; ];
?> ?>
@@ -82,7 +86,7 @@ $gridItems = [
</section> </section>
<section class="gallery-section" id="galerie" aria-label="<?= htmlspecialchars($t('gallery.aria'), ENT_QUOTES) ?>"> <section class="gallery-section" id="galerie" aria-label="<?= htmlspecialchars($t('gallery.aria'), ENT_QUOTES) ?>">
<div class="section-head"> <div class="section-head gallery-header">
<span class="section-eyebrow"><?= htmlspecialchars($t('gallery.eyebrow'), ENT_QUOTES) ?></span> <span class="section-eyebrow"><?= htmlspecialchars($t('gallery.eyebrow'), ENT_QUOTES) ?></span>
<h2><?= htmlspecialchars($t('gallery.h2'), ENT_QUOTES) ?></h2> <h2><?= htmlspecialchars($t('gallery.h2'), ENT_QUOTES) ?></h2>
</div> </div>
@@ -112,6 +116,13 @@ $gridItems = [
'attic' => 'bilder/grundrisse/Dachboden unten.png', 'attic' => 'bilder/grundrisse/Dachboden unten.png',
]; ];
$floorImageMapExtra = [
'eg' => ['img' => 'bilder/grundrisse/EG 3D.png', 'altKey' => 'floors.alt.eg_3d'],
'og1' => ['img' => 'bilder/grundrisse/OG 1 3D.png', 'altKey' => 'floors.alt.og1_3d'],
'og2' => ['img' => 'bilder/grundrisse/OG 2 3D.png', 'altKey' => 'floors.alt.og2_3d'],
'attic' => ['img' => 'bilder/grundrisse/Dachboden unten 2.png', 'altKey' => 'floors.alt.attic_2'],
];
$floors = [ $floors = [
['id' => 'eg', 'titleKey' => 'floors.eg.title', 'areaKey' => 'floors.eg.area', 'altKey' => 'floors.alt.eg', ['id' => 'eg', 'titleKey' => 'floors.eg.title', 'areaKey' => 'floors.eg.area', 'altKey' => 'floors.alt.eg',
'rooms' => [ 'rooms' => [
@@ -156,10 +167,23 @@ $gridItems = [
<span class="floor-icon" aria-hidden="true">+</span> <span class="floor-icon" aria-hidden="true">+</span>
</summary> </summary>
<div class="floor-body"> <div class="floor-body">
<img src="/<?= htmlspecialchars($floorImageMap[$floor['id']] ?? 'bilder/grundrisse/EG.png', ENT_QUOTES) ?>" <?php
alt="<?= htmlspecialchars($t($floor['altKey']), ENT_QUOTES) ?>" $primaryImg = $floorImageMap[$floor['id']] ?? 'bilder/grundrisse/EG.png';
loading="lazy" decoding="async" $extra = $floorImageMapExtra[$floor['id']] ?? null;
class="floor-plan-img"> $wrapperCls = $extra ? 'floor-plan-multi' : 'floor-plan';
?>
<div class="<?= htmlspecialchars($wrapperCls, ENT_QUOTES) ?>">
<img src="/<?= htmlspecialchars($primaryImg, ENT_QUOTES) ?>"
alt="<?= htmlspecialchars($t($floor['altKey']), ENT_QUOTES) ?>"
loading="lazy" decoding="async"
class="floor-plan-img">
<?php if ($extra !== null): ?>
<img src="/<?= htmlspecialchars($extra['img'], ENT_QUOTES) ?>"
alt="<?= htmlspecialchars($t($extra['altKey']), ENT_QUOTES) ?>"
loading="lazy" decoding="async"
class="floor-plan-img">
<?php endif; ?>
</div>
<ul class="room-list"> <ul class="room-list">
<?php foreach ($floor['rooms'] as $room): ?> <?php foreach ($floor['rooms'] as $room): ?>
<li> <li>
@@ -238,7 +262,7 @@ $gridItems = [
<div class="lage-map-wrapper"> <div class="lage-map-wrapper">
<iframe <iframe
title="<?= htmlspecialchars($t('loc.map_title'), ENT_QUOTES) ?>" title="<?= htmlspecialchars($t('loc.map_title'), ENT_QUOTES) ?>"
src="https://www.openstreetmap.org/export/embed.html?bbox=10.7535%2C50.5095%2C10.7705%2C50.5185&amp;layer=mapnik&amp;marker=50.5140%2C10.7620" src="https://www.openstreetmap.org/export/embed.html?bbox=10.7424%2C50.5072%2C10.7524%2C50.5108&amp;layer=mapnik&amp;marker=50.5090%2C10.7474"
loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe> loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>
<p class="lage-address"> <p class="lage-address">
<strong><?= htmlspecialchars($t('loc.address'), ENT_QUOTES) ?>:</strong><br> <strong><?= htmlspecialchars($t('loc.address'), ENT_QUOTES) ?>:</strong><br>

View File

@@ -543,17 +543,11 @@ nav.scrolled {
} }
.gallery-header { .gallery-header {
padding: 5rem 3rem 3rem; padding: 5rem 1.5rem 3rem;
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
} }
.gallery-section .section-head {
max-width: 1400px;
margin: 0 auto;
padding: 5rem 1.5rem 2.5rem;
}
.gallery-header h2 { .gallery-header h2 {
font-family: "Cormorant Garamond", serif; font-family: "Cormorant Garamond", serif;
font-size: clamp(2rem, 3vw, 2.8rem); font-size: clamp(2rem, 3vw, 2.8rem);
@@ -642,10 +636,6 @@ nav.scrolled {
.gallery-header { .gallery-header {
padding: 3rem 1.5rem 2rem; padding: 3rem 1.5rem 2rem;
} }
.gallery-section .section-head {
padding: 3rem 1rem 2rem;
}
} }
@media (width <= 480px) { @media (width <= 480px) {
@@ -670,63 +660,26 @@ nav.scrolled {
color: var(--charcoal); color: var(--charcoal);
} }
.floors-accordion { .floor-accordion {
display: flex; border-top: 1px solid var(--warm);
flex-direction: column;
gap: 0.75rem;
margin-top: -0.75rem;
} }
.floor-item { .floor-item {
border: 1px solid var(--warm); border-bottom: 1px solid var(--warm);
border-radius: 4px;
background: var(--white);
overflow: hidden; overflow: hidden;
position: relative;
transition:
border-color 0.25s ease,
box-shadow 0.25s ease;
}
.floor-item:hover {
border-color: var(--stone);
}
.floor-item.open {
border-color: var(--accent);
box-shadow: 0 4px 20px rgb(28 26 23 / 6%);
}
.floor-item.open::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: var(--accent);
} }
.floor-header { .floor-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
gap: 1rem; padding: 1.5rem 0;
padding: 1.25rem 1.5rem;
cursor: pointer; cursor: pointer;
list-style: none; transition: color 0.2s;
transition: background 0.2s ease;
} }
.floor-header::-webkit-details-marker { .floor-header:hover .floor-title {
display: none; color: var(--accent);
}
.floor-header::marker {
content: "";
}
.floor-header:hover {
background: var(--cream);
} }
.floor-title { .floor-title {
@@ -736,60 +689,41 @@ nav.scrolled {
transition: color 0.2s; transition: color 0.2s;
} }
.floor-header:hover .floor-title { .floor-size {
color: var(--accent);
}
.floor-area {
margin-left: auto;
font-size: 0.78rem; font-size: 0.78rem;
letter-spacing: 0.1em; letter-spacing: 0.1em;
color: var(--stone); color: var(--stone);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 1rem;
} }
.floor-icon { .floor-icon {
width: 32px; width: 24px;
height: 32px; height: 24px;
border: 1.5px solid var(--warm); border: 1px solid var(--warm);
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 1.15rem; font-size: 0.8rem;
font-weight: 300;
line-height: 1;
color: var(--stone); color: var(--stone);
background: var(--white);
flex-shrink: 0;
transition: transition:
transform 0.3s ease, transform 0.3s,
border-color 0.25s ease, border-color 0.3s;
color 0.25s ease,
background 0.25s ease;
}
.floor-header:hover .floor-icon {
border-color: var(--accent);
color: var(--accent);
} }
.floor-item.open .floor-icon { .floor-item.open .floor-icon {
transform: rotate(45deg); transform: rotate(45deg);
border-color: var(--accent); border-color: var(--accent);
color: var(--accent); color: var(--accent);
background: var(--cream);
} }
.floor-body { .floor-body {
display: none; display: none;
padding: 0 1.5rem 1.5rem; padding: 0 0 1.5rem;
max-width: 100%; max-width: 100%;
overflow: hidden; overflow: hidden;
border-top: 1px solid var(--warm);
margin-top: -1px;
} }
.floor-rooms-grid { .floor-rooms-grid {
@@ -813,7 +747,8 @@ nav.scrolled {
color: var(--stone); color: var(--stone);
} }
.floor-plan { .floor-plan,
.floor-plan-multi {
margin-top: 1.5rem; margin-top: 1.5rem;
border: 1px solid var(--warm); border: 1px solid var(--warm);
background: var(--white); background: var(--white);
@@ -821,14 +756,6 @@ nav.scrolled {
max-width: 100%; max-width: 100%;
} }
.floor-plan img {
width: 100%;
max-width: 100%;
height: auto;
display: block;
object-fit: contain;
}
.floor-plan-multi { .floor-plan-multi {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@@ -836,7 +763,9 @@ nav.scrolled {
background: var(--warm); background: var(--warm);
} }
.floor-plan-multi img { .floor-plan img,
.floor-plan-multi img,
img.floor-plan-img {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
height: auto; height: auto;
@@ -845,6 +774,42 @@ nav.scrolled {
background: var(--white); background: var(--white);
} }
/* Room list — visible m² per floor */
.room-list {
list-style: none;
margin: 1.5rem 0 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 0.5rem;
}
.room-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: var(--cream);
font-size: 0.82rem;
color: var(--charcoal);
}
.room-name {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.room-size {
flex: 0 0 auto;
font-size: 0.72rem;
color: var(--stone);
letter-spacing: 0.05em;
}
@media (width <= 768px) { @media (width <= 768px) {
.floor-plan-multi { .floor-plan-multi {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -859,11 +824,6 @@ nav.scrolled {
margin: 0 auto; margin: 0 auto;
} }
.pricing-inner {
max-width: 1200px;
margin: 0 auto;
}
.pricing-section .section-eyebrow { .pricing-section .section-eyebrow {
color: var(--accent-light); color: var(--accent-light);
} }
@@ -1116,31 +1076,27 @@ nav.scrolled {
margin: 0 auto; margin: 0 auto;
} }
.contact-inner { .contact-section h2 {
max-width: 700px;
margin: 0 auto;
text-align: center;
}
.contact-inner h2 {
font-family: "Cormorant Garamond", serif; font-family: "Cormorant Garamond", serif;
font-size: clamp(2.2rem, 4vw, 3.5rem); font-size: clamp(2.2rem, 4vw, 3.5rem);
font-weight: 300; font-weight: 300;
line-height: 1.15; line-height: 1.15;
color: var(--charcoal); color: var(--charcoal);
margin-bottom: 1rem; margin-bottom: 1rem;
text-align: center;
} }
.contact-inner h2 em { .contact-section h2 em {
font-style: italic; font-style: italic;
color: var(--accent); color: var(--accent);
} }
.contact-inner p { .contact-section p {
font-size: 0.9rem; font-size: 0.9rem;
color: var(--stone); color: var(--stone);
line-height: 1.8; line-height: 1.8;
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
text-align: center;
} }
.contact-form { .contact-form {
@@ -1740,39 +1696,34 @@ nav:not(.scrolled) .locale-switcher__trigger:focus-visible {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 6rem 3rem; padding: 6rem 3rem;
color: var(--dark); color: var(--stone-strong);
} line-height: 1.85;
.legal-page .section-eyebrow {
display: block;
margin-bottom: 1rem;
} }
.legal-page h1 { .legal-page h1 {
font-family: "Cormorant Garamond", serif; font-family: "Cormorant Garamond", serif;
font-size: clamp(2rem, 3.5vw, 3rem); font-size: clamp(2.4rem, 4vw, 3.4rem);
font-weight: 300; font-weight: 300;
line-height: 1.2; line-height: 1.1;
margin-bottom: 2.5rem;
color: var(--charcoal); color: var(--charcoal);
margin: 0.5rem 0 2.5rem;
} }
.legal-page h2 { .legal-page h2 {
font-family: "DM Sans", sans-serif; font-family: "DM Sans", sans-serif;
font-size: 1.1rem; font-size: 0.78rem;
font-weight: 600; font-weight: 600;
letter-spacing: 0.04em; letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--charcoal); color: var(--charcoal);
margin: 2rem 0 0.75rem; margin: 2.5rem 0 1rem;
} }
.legal-page h3 { .legal-page h3 {
font-family: "DM Sans", sans-serif; font-family: "DM Sans", sans-serif;
font-size: 0.85rem; font-size: 0.95rem;
font-weight: 600; font-weight: 600;
letter-spacing: 0.06em; color: var(--charcoal);
text-transform: uppercase;
color: var(--stone-strong);
margin: 1.5rem 0 0.5rem; margin: 1.5rem 0 0.5rem;
} }
@@ -1782,11 +1733,7 @@ nav:not(.scrolled) .locale-switcher__trigger:focus-visible {
font-size: 0.95rem; font-size: 0.95rem;
line-height: 1.85; line-height: 1.85;
color: var(--stone-strong); color: var(--stone-strong);
}
.legal-page address {
font-style: normal; font-style: normal;
margin-bottom: 1rem;
} }
.legal-page ul { .legal-page ul {
@@ -1796,47 +1743,43 @@ nav:not(.scrolled) .locale-switcher__trigger:focus-visible {
} }
.legal-page ul li { .legal-page ul li {
margin-bottom: 0.25rem; margin-bottom: 0.35rem;
} }
.legal-page a { .legal-page a {
color: var(--accent); color: var(--accent);
text-decoration: none; text-decoration: underline;
border-bottom: 1px solid rgb(139 105 20 / 25%); text-underline-offset: 0.2em;
transition: text-decoration-thickness: 1px;
border-color 0.2s,
color 0.2s;
} }
.legal-page a:hover { .legal-page a:hover {
color: var(--accent-strong); color: var(--accent-light);
border-bottom-color: var(--accent-strong);
} }
.legal-divider { .legal-divider {
border: none; border: 0;
border-top: 1px solid var(--warm); border-top: 1px solid var(--warm);
margin: 2.5rem 0; margin: 2rem 0;
} }
.legal-back { .legal-back {
display: inline-block; display: inline-block;
margin-top: 2.5rem; margin-top: 3rem;
font-size: 0.78rem; font-family: "DM Sans", sans-serif;
font-size: 0.72rem;
font-weight: 600; font-weight: 600;
letter-spacing: 0.15em; letter-spacing: 0.18em;
text-transform: uppercase; text-transform: uppercase;
color: var(--accent); color: var(--accent);
border-bottom: 1px solid var(--accent); text-decoration: none;
padding-bottom: 2px; padding-bottom: 0.25rem;
transition: border-bottom: 1px solid currentcolor;
color 0.2s, transition: color 0.2s;
border-color 0.2s;
} }
.legal-back:hover { .legal-back:hover {
color: var(--accent-strong); color: var(--accent-light);
border-bottom-color: var(--accent-strong);
} }
@media (width <= 768px) { @media (width <= 768px) {

View File

@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
namespace Tests\Views;
use PHPUnit\Framework\TestCase;
/**
* Regression test for issue #80: all house images in public/bilder/ must be
* referenced from the home view. Catches drift when new photos are added to
* disk but not yet wired into the gallery or floor-plan map.
*/
final class HomeGalleryInventoryTest extends TestCase
{
private const VIEW_PATH = __DIR__ . '/../../app/views/home/index.php';
private const BILDER_DIR = __DIR__ . '/../../public/bilder';
/** @var list<string> Files that may exist on disk but are intentionally NOT shown on the page. */
private const ALLOWED_UNUSED = [
// Hero / page background uses a .webp variant; the .png in bilder/ is the
// gallery source. Hero-bg is referenced separately in the <header>.
'favicon/',
// -small.* variants are served as thumb hints by JS lazy-load optimization.
'*-small.*',
];
public function testAllBilderFilesAreReferencedInHomeView(): void
{
$view = file_get_contents(self::VIEW_PATH);
self::assertNotFalse($view, 'Could not read home view');
$diskFiles = $this->listBilderFiles();
self::assertNotEmpty($diskFiles, 'No files found in public/bilder/');
// Group files by base name (filename without extension) so the
// .jpg/.png/.jpeg source + auto-generated .webp variant count as ONE image.
$groups = [];
foreach ($diskFiles as $relPath) {
if ($this->isAllowedUnused($relPath)) {
continue;
}
$groups[$this->baseOf($relPath)][] = $relPath;
}
$missing = [];
foreach ($groups as $base => $variants) {
$hit = false;
foreach ($variants as $v) {
// View may reference raw or with leading slash.
if (str_contains($view, $v) || str_contains($view, '/' . ltrim($v, '/'))) {
$hit = true;
break;
}
}
if (!$hit) {
$missing[] = $base . ' (' . implode(', ', $variants) . ')';
}
}
self::assertSame(
[],
$missing,
sprintf(
"These image groups exist in public/bilder/ but are NOT referenced anywhere in the home view:\n - %s\n\nFix: add them to \$gridItems (gallery) or \$floorImageMap / \$floorImageMapExtra (floor plans), and add the matching Locale keys.",
implode("\n - ", $missing),
)
);
}
public function testGalleryContainsAllRequiredImages(): void
{
$view = file_get_contents(self::VIEW_PATH);
self::assertNotFalse($view, 'Could not read home view');
$required = [
// Bathrooms (3 new ones in addition to the existing Bad.jpg)
'bilder/Bad.jpg',
'bilder/Bad-2.jpeg',
'bilder/Bad-3.jpeg',
'bilder/Bad-4.jpeg',
// Kids room extras
'bilder/Kinderzimmer.png',
// Floor plans 2D
'bilder/grundrisse/EG.png',
'bilder/grundrisse/OG 1 2.png',
'bilder/grundrisse/OG 2 grundriss.png',
'bilder/grundrisse/Dachboden unten.png',
// Floor plans 3D / alternate
'bilder/grundrisse/EG 3D.png',
'bilder/grundrisse/OG 1 3D.png',
'bilder/grundrisse/OG 2 3D.png',
'bilder/grundrisse/Dachboden unten 2.png',
];
$missing = array_values(array_filter($required, static fn (string $img) => !str_contains($view, $img)));
self::assertSame(
[],
$missing,
sprintf(
"Required images missing from home view:\n - %s",
implode("\n - ", $missing),
)
);
}
public function testFloorPlanMapsCoverAllFloors(): void
{
$view = file_get_contents(self::VIEW_PATH);
self::assertNotFalse($view, 'Could not read home view');
// Each floor in the $floors array must have an entry in BOTH maps
// (primary 2D + extra 3D/alternate) so the floor body renders both.
foreach (['eg', 'og1', 'og2', 'attic'] as $floorId) {
self::assertStringContainsString(
"'{$floorId}'",
$view,
"Floor '{$floorId}' not declared in home view",
);
}
}
/**
* @return list<string> relative paths (forward-slash, e.g. "grundrisse/EG.png")
*/
private function listBilderFiles(): array
{
$files = [];
$iter = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(self::BILDER_DIR, \FilesystemIterator::SKIP_DOTS),
);
foreach ($iter as $file) {
if (!$file->isFile()) {
continue;
}
$rel = substr($file->getPathname(), strlen(self::BILDER_DIR) + 1);
$rel = str_replace(DIRECTORY_SEPARATOR, '/', $rel);
$files[] = $rel;
}
sort($files);
return $files;
}
private function isAllowedUnused(string $relPath): bool
{
foreach (self::ALLOWED_UNUSED as $pattern) {
if (str_ends_with($pattern, '/')) {
// Directory prefix
if (str_starts_with($relPath, $pattern)) {
return true;
}
} else {
// Glob-style match (only `*` supported, for `-small.*` variants)
$regex = '#^' . str_replace('\\*', '.*', preg_quote($pattern, '#')) . '$#';
if (preg_match($regex, $relPath) === 1) {
return true;
}
}
}
return false;
}
/**
* Base name = path minus extension. So "grundrisse/EG 3D.png" → "grundrisse/EG 3D",
* and "Küche 1.jpg" → "Küche 1". Used to group source + auto-generated variants.
*/
private function baseOf(string $relPath): string
{
$dot = strrpos($relPath, '.');
if ($dot === false || $dot === 0) {
return $relPath;
}
return substr($relPath, 0, $dot);
}
}