Kachel-Mindesthoehe war fix auf 640px, dadurch ragten Pakete auf dem Handy
weit ueber den Bildschirm hinaus (Kundenfeedback). Jetzt 420px auf Mobile,
640px ab Desktop-Breite.
Der Mausrad-Hinweis lief bisher sofort beim Laden ab, oft bevor die
Kartenreihe ueberhaupt sichtbar gescrollt war - deshalb kam er beim Kunden
nicht an. Spielt jetzt erst ab, sobald die Reihe tatsaechlich im Blickfeld ist.
Leistungen-Karten ohne eigenen CTA (alles ausser Car-Hifi) hatten gar keinen
Link. Fallen jetzt auf das Kontaktformular zurueck statt ins Leere zu laufen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A single long Google review made every card in the row stretch to match
it (default flex align-items: stretch), leaving short reviews with a
huge empty gap before the name - reported with a real screenshot where
one card towered over its neighbors.
The review paragraph is now capped at line-clamp-6, so no single card's
natural height is unbounded anymore - stretch still aligns the row, but
to a reasonable, consistent height instead of an outlier's full text.
Full text stays in the DOM (still copyable/accessible), just visually
truncated with a native ellipsis.
Added a bit more visual polish while in there: a subtle decorative quote
mark per card, a hover shadow lift, and an avatar next to each name -
the reviewer's real Google profile photo when available, otherwise a
colored initial-letter circle (Home.jsx now passes profile_photo_url
through, previously fetched but unused).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The homepage's "reviews" section was never real - a fixed array of 10
hand-written quotes in the translation files plus a manually-typed
"5,0 von 181 Kunden bewertet" string, none of it sourced from Google.
Wires it to the real thing via the Places API (New), cached in the
database and refreshed on a schedule rather than fetched live per
visitor - the public /google-reviews endpoint only ever reads the last
successfully cached result, so page traffic never touches Google's API
or its quota. Mirrors the existing audio4cars price-refresh cron exactly
(same bootstrap-config-fetch-store shape): refresh_google_reviews.php
reads the Place ID + API key from app_settings, calls
GooglePlacesReviewsFetcher, and replaces the google_reviews table
wholesale on success; refresh_google_reviews.bat is the Task Scheduler
entrypoint. A failed refresh leaves the previous good data in place and
only records the error message, so a bad night never blanks the section.
Admin config lives in a new "Google-Rezensionen" block on the Website
settings page (Place ID + API key, matching the existing GA
service-account field's "leave blank to keep" behavior for the key) plus
a "Jetzt aktualisieren" button to test immediately rather than wait for
the nightly job - both hit the same GoogleReviewsController the cron
script's service class does.
Home.jsx now fetches /google-reviews and swaps in the live rating and
quotes only once at least one fetched review has actual text (Google
sometimes returns star-only reviews with no comment, which StarRating's
quote cards can't render) - until then, or if nothing is configured yet,
it falls back to exactly the previous hardcoded testimonials, so the
section never looks broken mid-rollout. TestimonialSlider/StarRating now
take a per-review rating instead of always drawing 5 stars.
Verified end-to-end except the live Google call itself, which needs a
real API key: schema migration, settings save/load (including the
key's "blank = unchanged" behavior), the CLI script's two failure paths
(unconfigured, and a real Google 400 for an invalid key - confirming the
Places API (New) request shape is correct), and the full display path by
seeding fake "successful" rows directly - homepage correctly switched to
the live rating/quotes and back to the fallback after clearing them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Import always replaced everything - brands, packages, services, FAQs,
gallery - in one irreversible sweep, even if you only wanted to bring in
one part (e.g. restoring just the gallery from an old backup while
leaving today's live catalog alone).
Backend: importData() now accepts a "sections" field (JSON array of
catalog/services/faqs/gallery) and only DELETE+INSERTs the tables that
belong to selected sections; anything not selected is left completely
untouched. Tables stay grouped the way their foreign keys require
(brands -> car_models -> packages -> package_products/upgrades as one
unit, gallery_brands -> gallery_projects -> gallery_photos as another) -
importing "packages" without its models would either orphan rows or
silently rewrite unrelated ones, so the groups aren't splittable further.
Restored images are filtered to just the ones referenced by the tables
actually being imported, so deselecting the gallery means its images
aren't written to /uploads either. No sections field (older callers)
still imports everything, matching prior behavior.
Frontend: picking a file parses it client-side (file.text() + JSON.parse)
to show each section with live counts pulled straight from the file
("4 Marken, 15 Modelle, 17 Pakete"), all checked by default. The
confirmation checkbox's warning text names only what's actually
selected. If parsing fails client-side the picker just doesn't render -
the import still goes through server-side with the (safe) default of
importing everything.
Verified end-to-end against a real ~54MB export: importing only "faqs"
reverted a changed FAQ while a newly added test brand survived
untouched; importing only "catalog" right after removed that test brand
while the FAQ stayed as already restored - confirming sections are
fully independent both ways.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The existing peek animation (row nudges right and back on first load)
only demonstrated that the row moves, not how to move it - ambiguous
now that mouse-drag is gone. On non-touch devices it's paired with a
small pill ("Mit dem Mausrad blättern" / "Scroll to browse", mouse icon
flanked by chevrons) that fades in around the peek and out ~1.2s later,
floating centered over the row at a z-index above even the frontmost
coverflow card. Touch devices keep just the peek, since swiping is
already self-explanatory there. Plays once per page load, gated behind
the same reduced-motion check as the peek itself.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Client feedback: a light/small scroll snapped back to the current card
instead of advancing - native scroll-snap always settles on whichever
card is nearest, and a light nudge rarely crosses the halfway point.
Each wheel burst now tracks its starting card index and net deltaY.
On settle, if the nearest card by position is still the start card but
there was clear directional movement (>4px net), the row advances one
card in that direction instead of springing back - so even a light
wheel tick pages forward. Larger scrolls are unaffected: if the actual
scroll position already lands closer to a farther card, it jumps
straight there (no artificial one-card-per-burst cap), and a burst that
nets out near zero (e.g. wiggling back and forth) correctly stays put.
The settle target is computed the same way the old drag-to-scroll
feature centered cards, and re-enables scroll-snap only after the
settling scrollTo animation would have finished, so it doesn't fight
the smooth scroll.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Client feedback: after removing mouse-drag on the coverflow (it fought
the 3D transform), the thin scrollbar underneath was the only desktop
way to browse tiles - too fiddly.
Hovering the tile row and scrolling the wheel now moves the row
horizontally, fluid/1:1 with the wheel delta rather than snapping one
card per tick, with scroll-snap briefly suspended during the burst and
re-enabled ~150ms after the last tick so cards settle into place
afterward - the same pattern already used for the old drag interaction.
At the first card, scrolling further "up" is left alone (no
preventDefault): the event falls through to native behavior, which is
already exactly how the page scrolls today when hovering the row, so
the page scrolls up instead. Same at the last card scrolling "down" -
the page takes over. Horizontal wheel/trackpad sideswipe (deltaX) is
untouched, since the container already handles that natively. Applies
uniformly to both strip and coverflow (same scrollerRef container); the
scrollbar stays as-is.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Side-by-side with the prototype revealed the section never set its font
family - everything except the Michroma headings fell back to the system
font, which was the biggest visual mismatch (labels, subtitle, feature
bar). The section root now sets Barlow like the handoff specifies; the
subtitle even wraps identically to the prototype again.
The maps iframe crop is rebalanced (290px shifted -60px inside the 210px
window) so the map runs flush to the "Route planen" bar with Google's
bottom attribution cut at the card edge like the prototype, instead of
showing the full attribution line with a gap. The contact card is titled
"HiFi Planet Amorbach" per the design (was "HifiPlanet Amorbach").
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Third iteration on this backdrop: composing the layers natively (photo,
scrim, masks, streak stack, topo lines, grunge/splatter/arc) kept
drifting from the intended look, so the handoff now ships each theme's
background as a single pre-composed 2560x1440 image. All hand-built
layers are deleted; the section carries exactly one background-image
via .kontakt-sektion (cover/center), swapped for dark mode through the
existing .dark class. Obsolete layer assets are removed, the unused
imports with them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 310x512 car-light.png was scaled to the full section height via
background-size: cover, which blew it up far past its natural
resolution. It is now a plain <img> (.kontakt-bg-foto in index.css):
natural aspect ratio, width capped at clamp(280px, 24vw, 460px),
anchored bottom-left, opacity .9, and a dual mask (fade right + fade
top, mask-composite: intersect) so it dissolves into the page gradient
with no hard edges. The old scrim overlay is gone - the masks do that
job now.
The green light-streak group on the right is widened to the README's
42% at full section height (was 26%), line opacity .85 and radial glow
blur 20px per spec. Topo lines were already in place.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The redesign itself shipped earlier (78d3d4a, 3079bd2); comparing the
implementation against the README again surfaced two missing details:
- The light-mode backdrop's subtle topo wave lines (five 1px curved
paths in --kf-topo, top-left, ~360x280) were never implemented.
- The Google Maps embed showed Google's own info overlay in the top-left
corner behind our "In Maps öffnen" chip. The iframe is now rendered
taller inside a clipped 210px container with a negative top margin,
cropping that overlay away while keeping the marker and the required
bottom attribution visible.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Client wants the tier called just "Elite". Only the admin checkbox list
(PACKAGE_TIERS) carries tier names; public card theming is price-rank
based, so nothing else changes. An already-created "Elite R" package
would show up under "Weitere Pakete (alte Namen)" in the editor and can
be deleted and recreated as Elite there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The client's hosting (All-Inkl) has no GitHub integration, so the
two-branch model - source on main, CI force-pushing the built hifi/
folder to a machine-generated deploy branch for Plesk to pull - doesn't
work there. main is now simultaneously the source of truth and the
deployable artifact:
- hifi/index.html, hifi/assets/, favicon, robots.txt and hifi/api/vendor
are committed instead of git-ignored. Secrets (db.php, setup.php,
ga-service-account.json) stay ignored as before.
- A prebuild script wipes hifi/assets + index.html before every vite
build, so the tree always holds exactly the current build - this was
the stale-hash-chunk problem that originally justified ignoring the
output (emptyOutDir stays false to protect hifi/api). Builds are
deterministic: rebuilding without source changes yields zero diffs.
- .github/workflows/build-deploy.yml is deleted; nothing writes to the
deploy branch anymore. The convention is to run npm run build before
committing (DEPLOY.md documents the workflow, the All-Inkl setup via
SSH clone/pull with docroot on <checkout>/hifi, and the Plesk test
server switch from deploy to main).
The remote deploy branch is left in place for now so the Plesk test
server keeps working until its Git source is switched to main; it can
be deleted afterwards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>