feat: Google-Rezensionen als eigener Abschnitt auf der Onepage

- ReviewsSection (Server Component) mit Durchschnitt, zugaenglichen
  Sternen, Initialen statt Profilbildern, relativer Zeit, Links zur
  Rezension und zum Maps-Eintrag sowie Hinweis "Rezensionen von Google"
- lange Texte optisch gekuerzt, "Weiterlesen" mit aria-expanded
- ohne Daten ausgeblendet; in Entwicklung/Staging ein Einrichtungshinweis
- Laden parallel zu loadPublicPage, Fehler blenden nur den Abschnitt aus
- Links aus der API-Antwort nur zu HTTPS-Google-Hosts (safeGoogleUrl)
- Linkstile und Browser-Oberflaechen (Scrollbar, Caret, Unterstreichung)

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-28 17:46:19 +02:00
1 parent b4eafbe433
commit f74a08fa68
8 files changed
+367 -5

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
/**
* Anzeigehilfen fuer Google-Rezensionen. Reine Funktionen ohne
* Seiteneffekte, damit sie ohne Netzwerk testbar sind.
*/
/** Ab dieser Laenge wird eine Rezension zunaechst gekuerzt angezeigt. */
export const REVIEW_CLAMP_CHARS = 320;
/** Initialen statt Profilbild: bis zu zwei Buchstaben aus dem Anzeigenamen. */
export function initialsOf(name: string): string {
const letters = name
.trim()
.split(/\s+/)
.map((part) => Array.from(part.replace(/[^\p{L}\p{N}]/gu, ""))[0] ?? "")
.filter(Boolean);
if (letters.length === 0) return "?";
const pick = letters.length === 1 ? [letters[0]] : [letters[0], letters[letters.length - 1]];
return pick.join("").toLocaleUpperCase("de-DE");
}
/** "4,8" – Durchschnitt mit einer Nachkommastelle im deutschen Format. */
export function formatRating(rating: number): string {
return rating.toLocaleString("de-DE", { minimumFractionDigits: 1, maximumFractionDigits: 1 });
}
/** Zugaenglicher Text fuer eine Sternebewertung, z. B. "4 von 5 Sternen". */
export function starsLabel(rating: number): string {
const value = Number.isInteger(rating) ? String(rating) : formatRating(rating);
return `${value} von 5 Sternen`;
}
/** "1 Rezension" bzw. "12 Rezensionen". */
export function reviewCountLabel(count: number): string {
return `${count.toLocaleString("de-DE")} ${count === 1 ? "Rezension" : "Rezensionen"}`;
}
export function needsClamp(text: string): boolean {
return text.length > REVIEW_CLAMP_CHARS;
}