diff --git a/hifi-src/src/components/TestimonialSlider.jsx b/hifi-src/src/components/TestimonialSlider.jsx
index 90cd44b..9d433f5 100644
--- a/hifi-src/src/components/TestimonialSlider.jsx
+++ b/hifi-src/src/components/TestimonialSlider.jsx
@@ -124,7 +124,7 @@ export default function TestimonialSlider({ testimonials }) {
{track.map((t, i) => (
diff --git a/hifi-src/src/i18n/de.js b/hifi-src/src/i18n/de.js
index 6e06566..dea453b 100644
--- a/hifi-src/src/i18n/de.js
+++ b/hifi-src/src/i18n/de.js
@@ -103,6 +103,7 @@ export default {
'Individueller Subwoofer-Einbau mit LED-Beleuchtung im Kofferraum',
],
reviewsRating: '5,0 von 181 Kunden bewertet',
+ reviewsRatingDynamic: (rating, count) => `${rating.toFixed(1).replace('.', ',')} von ${count} Kunden bewertet`,
reviewsText: 'Das sagen unsere Kunden über uns.',
reviewsLink: 'Alle Bewertungen auf Google ansehen →',
testimonials: [
diff --git a/hifi-src/src/i18n/en.js b/hifi-src/src/i18n/en.js
index f99b06a..1af6054 100644
--- a/hifi-src/src/i18n/en.js
+++ b/hifi-src/src/i18n/en.js
@@ -103,6 +103,7 @@ export default {
'Custom subwoofer installation with LED lighting in the trunk',
],
reviewsRating: 'Rated 5.0 by 181 customers',
+ reviewsRatingDynamic: (rating, count) => `Rated ${rating.toFixed(1)} by ${count} customers`,
reviewsText: 'Here\'s what our customers say about us.',
reviewsLink: 'See all reviews on Google →',
testimonials: [
diff --git a/hifi-src/src/pages/admin/settings/WebsiteSettings.jsx b/hifi-src/src/pages/admin/settings/WebsiteSettings.jsx
index 6afdc12..0e7dc4e 100644
--- a/hifi-src/src/pages/admin/settings/WebsiteSettings.jsx
+++ b/hifi-src/src/pages/admin/settings/WebsiteSettings.jsx
@@ -38,6 +38,15 @@ export default function WebsiteSettings() {
const [gaError, setGaError] = useState('');
const [gaSaved, setGaSaved] = useState(false);
+ const [reviewsPlaceId, setReviewsPlaceId] = useState('');
+ const [reviewsApiKey, setReviewsApiKey] = useState('');
+ const [reviewsHasApiKey, setReviewsHasApiKey] = useState(false);
+ const [reviewsStatus, setReviewsStatus] = useState(null); // { rating, rating_count, updated_at, error }
+ const [reviewsSaveBusy, setReviewsSaveBusy] = useState(false);
+ const [reviewsSaveError, setReviewsSaveError] = useState('');
+ const [reviewsSaved, setReviewsSaved] = useState(false);
+ const [reviewsRefreshBusy, setReviewsRefreshBusy] = useState(false);
+
useEffect(() => {
api
.get('/site-settings')
@@ -57,8 +66,51 @@ export default function WebsiteSettings() {
setGaPropertyId(res.ga_property_id || '');
setGaHasCredentials(res.has_credentials);
});
+ loadReviewsStatus();
}, []);
+ const loadReviewsStatus = () => {
+ api.get('/settings/google-reviews').then((res) => {
+ setReviewsPlaceId(res.place_id || '');
+ setReviewsHasApiKey(res.has_api_key);
+ setReviewsStatus(res);
+ });
+ };
+
+ const handleReviewsSave = async () => {
+ setReviewsSaveBusy(true);
+ setReviewsSaveError('');
+ setReviewsSaved(false);
+ try {
+ const res = await api.post('/settings/google-reviews', {
+ place_id: reviewsPlaceId,
+ api_key: reviewsApiKey,
+ });
+ setReviewsHasApiKey(res.has_api_key);
+ setReviewsStatus(res);
+ setReviewsApiKey('');
+ setReviewsSaved(true);
+ } catch (err) {
+ setReviewsSaveError(err.message);
+ } finally {
+ setReviewsSaveBusy(false);
+ }
+ };
+
+ const handleReviewsRefresh = async () => {
+ setReviewsRefreshBusy(true);
+ setReviewsSaveError('');
+ try {
+ const res = await api.post('/settings/google-reviews/refresh', {});
+ setReviewsStatus(res);
+ } catch (err) {
+ setReviewsSaveError(err.message);
+ loadReviewsStatus(); // Fehler wurde serverseitig auch in app_settings vermerkt - Status nachladen
+ } finally {
+ setReviewsRefreshBusy(false);
+ }
+ };
+
const handleGaDashboardSave = async () => {
setGaBusy(true);
setGaError('');
@@ -276,6 +328,95 @@ export default function WebsiteSettings() {
+
+ Google-Rezensionen
+
+ Zeigt die echte Google-Bewertung und die aktuellsten Rezensionen auf der Startseite an, statt der
+ fest eingetragenen Beispiel-Texte. Wird einmal täglich automatisch aktualisiert (nie bei einem
+ Seitenbesuch selbst) – die Seite zeigt also immer den letzten erfolgreich abgerufenen Stand.
+
+
+
+
Place-ID
+
{ setReviewsPlaceId(e.target.value); setReviewsSaved(false); }}
+ placeholder="z. B. ChIJN1t_tDeuEmsRUsoyG83frY4"
+ className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
+ />
+
+ Eindeutige Google-Kennung eures Eintrags – zu finden über den{' '}
+
+ Google Place ID Finder
+
+ .
+
+
+
+
+ Places-API-Key {reviewsHasApiKey ? '(leer lassen = unverändert)' : ''}
+
+
{ setReviewsApiKey(e.target.value); setReviewsSaved(false); }}
+ placeholder={reviewsHasApiKey ? '••••••••••••••••••••• (bereits hinterlegt)' : 'AIzaSy…'}
+ className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 font-mono text-sm dark:border-neutral-700 dark:bg-neutral-900"
+ />
+
+ Aus der Google-Cloud-Console, mit aktivierter „Places API (New)“.
+
+
+
+
+ {reviewsSaveError && {reviewsSaveError}
}
+ {reviewsSaved && Gespeichert.
}
+
+
+
+ {reviewsSaveBusy ? 'Speichere…' : 'Google-Rezensionen-Einstellungen speichern'}
+
+
+ {reviewsRefreshBusy ? 'Aktualisiere…' : 'Jetzt aktualisieren'}
+
+
+
+ {reviewsStatus && (
+
+ {reviewsStatus.rating != null ? (
+
+ Aktuell hinterlegt: {reviewsStatus.rating} ★ ({reviewsStatus.rating_count} Bewertungen)
+ {reviewsStatus.updated_at && (
+ <> – zuletzt aktualisiert am {new Date(reviewsStatus.updated_at.replace(' ', 'T')).toLocaleString('de-DE')}>
+ )}
+
+ ) : (
+
+ Noch keine erfolgreiche Aktualisierung – die Startseite zeigt bis dahin weiter die
+ Beispiel-Rezensionen.
+
+ )}
+ {reviewsStatus.error && (
+
Letzter Fehler: {reviewsStatus.error}
+ )}
+
+ )}
+
+
{error && Gespeichert.
}
diff --git a/hifi-src/src/pages/public/Home.jsx b/hifi-src/src/pages/public/Home.jsx
index d99757a..27efafa 100644
--- a/hifi-src/src/pages/public/Home.jsx
+++ b/hifi-src/src/pages/public/Home.jsx
@@ -45,8 +45,18 @@ export default function Home() {
const stats = t('home.stats');
const galleryAlts = t('home.galleryAlts');
const gallery = galleryImages.map((img, i) => ({ ...img, alt: galleryAlts[i] }));
- const testimonials = t('home.testimonials');
const [faqs, setFaqs] = useState([]);
+ // Solange noch keine Google-Rezensionen konfiguriert/erfolgreich abgerufen wurden
+ // (googleReviews === null), zeigt die Seite die festen Beispiel-Texte aus den
+ // Uebersetzungen - danach die echten, im Admin-Bereich zwischengespeicherten Rezensionen.
+ const [googleReviews, setGoogleReviews] = useState(null);
+ const testimonials = googleReviews
+ ? googleReviews.reviews
+ // Manche Google-Rezensionen sind reine Sterne-Bewertungen ohne Text - als
+ // Zitat-Karte ohne Zitat waeren die nur eine leere Huelle.
+ .filter((r) => r.review_text)
+ .map((r) => ({ name: r.author_name, text: r.review_text, rating: r.rating }))
+ : t('home.testimonials');
useEffect(() => {
api
@@ -62,6 +72,17 @@ export default function Home() {
.catch(() => {});
}, [language]);
+ useEffect(() => {
+ api
+ .get('/google-reviews')
+ .then((res) => {
+ // Nur uebernehmen, wenn mindestens eine Rezension auch echten Text hat -
+ // sonst bliebe die Zitat-Karten-Liste leer (siehe testimonials-Filter oben).
+ if (res.rating != null && res.reviews.some((r) => r.review_text)) setGoogleReviews(res);
+ })
+ .catch(() => {});
+ }, []);
+
usePageMeta({
title: t('home.metaTitle'),
description: t('home.metaDescription'),
@@ -298,9 +319,11 @@ export default function Home() {