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>