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:
1 parent
b4eafbe433
commit
f74a08fa68
8 files changed
+367
-5
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user